From c5eef106971ce4c2ae3e758e6becdcf6f284c28e Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 3 Feb 2026 15:42:35 -0500 Subject: [PATCH 01/16] Core and UI: Implement missing service functions and QLaunch improvements --- src/citron/configuration/qt_config.h | 2 +- src/citron/game_list.cpp | 1924 ++++++++++------- src/citron/game_list.h | 28 +- src/citron/game_list_worker.cpp | 202 +- src/citron/main.cpp | 641 +++--- src/citron/main.h | 205 +- src/citron/main.ui | 7 + src/core/CMakeLists.txt | 7 +- src/core/file_sys/registered_cache.cpp | 65 +- src/core/file_sys/registered_cache.h | 10 +- .../hle/kernel/svc/svc_synchronization.cpp | 6 +- src/core/hle/service/acc/acc.cpp | 82 +- src/core/hle/service/am/am_types.h | 3 +- src/core/hle/service/am/applet.h | 1 + src/core/hle/service/am/applet_manager.cpp | 17 + src/core/hle/service/am/applet_manager.h | 4 + .../hle/service/am/display_layer_manager.cpp | 40 + .../hle/service/am/display_layer_manager.h | 2 + .../service/am/frontend/applet_general.cpp | 4 + .../hle/service/am/frontend/applet_general.h | 1 + .../all_system_applet_proxies_service.cpp | 21 +- .../all_system_applet_proxies_service.h | 25 +- .../service/applet_alternative_functions.cpp | 30 + .../am/service/applet_alternative_functions.h | 23 + .../am/service/applet_common_functions.cpp | 9 +- .../am/service/applet_common_functions.h | 1 + .../am/service/common_state_getter.cpp | 13 +- .../service/am/service/common_state_getter.h | 1 + .../am/service/library_applet_creator.cpp | 18 +- .../am/service/overlay_applet_proxy.cpp | 100 +- .../service/am/service/overlay_applet_proxy.h | 20 +- .../service/am/service/overlay_functions.cpp | 128 ++ .../service/am/service/overlay_functions.h | 30 + src/core/hle/service/am/window_system.cpp | 15 +- src/core/hle/service/am/window_system.h | 5 + .../hle/service/aoc/addon_content_manager.cpp | 79 +- .../hle/service/aoc/addon_content_manager.h | 4 + src/core/hle/service/caps/caps_a.cpp | 9 + src/core/hle/service/caps/caps_a.h | 2 + .../hle/service/filesystem/fsp/fsp_srv.cpp | 39 +- src/core/hle/service/filesystem/fsp/fsp_srv.h | 7 +- src/core/hle/service/nifm/nifm.cpp | 91 +- src/core/hle/service/nifm/nifm.h | 5 + src/core/hle/service/npns/npns.cpp | 77 +- .../ns/application_manager_interface.cpp | 247 ++- .../ns/application_manager_interface.h | 16 +- src/core/hle/service/ns/ns_types.h | 51 +- src/core/hle/service/ns/query_service.cpp | 72 +- src/core/hle/service/ns/query_service.h | 26 + ...nly_application_control_data_interface.cpp | 316 +++ ..._only_application_control_data_interface.h | 11 +- ...read_only_application_record_interface.cpp | 15 + .../read_only_application_record_interface.h | 3 + .../service/ns/service_getter_interface.cpp | 22 +- src/core/hle/service/nvdrv/core/container.cpp | 44 +- src/core/hle/service/nvdrv/core/container.h | 3 +- .../hle/service/nvdrv/nvdrv_interface.cpp | 1 - src/core/hle/service/nvnflinger/display.h | 4 +- .../service/nvnflinger/hardware_composer.cpp | 2 +- .../service/nvnflinger/surface_flinger.cpp | 7 + .../hle/service/nvnflinger/surface_flinger.h | 4 +- .../service/pctl/parental_control_service.cpp | 1 + src/core/hle/service/psc/ovln/ovln_types.h | 8 + src/core/hle/service/psc/ovln/receiver.cpp | 99 +- src/core/hle/service/psc/ovln/receiver.h | 17 +- src/core/hle/service/set/settings_types.h | 9 + .../service/set/system_settings_server.cpp | 14 +- .../hle/service/set/system_settings_server.h | 1 + src/core/hle/service/vi/container.cpp | 43 + src/core/hle/service/vi/container.h | 3 + .../service/vi/manager_display_service.cpp | 4 + .../hle/service/vi/manager_display_service.h | 1 + 72 files changed, 3529 insertions(+), 1518 deletions(-) create mode 100644 src/core/hle/service/am/service/applet_alternative_functions.cpp create mode 100644 src/core/hle/service/am/service/applet_alternative_functions.h create mode 100644 src/core/hle/service/am/service/overlay_functions.cpp create mode 100644 src/core/hle/service/am/service/overlay_functions.h diff --git a/src/citron/configuration/qt_config.h b/src/citron/configuration/qt_config.h index 583985780..3d9eda4b5 100644 --- a/src/citron/configuration/qt_config.h +++ b/src/citron/configuration/qt_config.h @@ -44,7 +44,7 @@ protected: void SaveUIValues() override; void SaveUIGamelistValues() override; void SaveUILayoutValues() override; - void SaveMultiplayerValues(); + void SaveMultiplayerValues() override; void SaveNetworkValues(); public: diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 6320ad473..cfaeeb1d0 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -2,64 +2,62 @@ // SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include +#include #include #include +#include +#include #include +#include +#include +#include #include +#include +#include #include #include #include #include #include +#include #include #include -#include -#include #include -#include #include #include -#include -#include -#include -#include #include -#include -#include +#include +#include +#include +#include #include +#include +#include #include #include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include "common/common_types.h" -#include "common/logging/log.h" -#include "common/string_util.h" -#include "core/core.h" -#include "core/file_sys/patch_manager.h" -#include "core/file_sys/registered_cache.h" -#include "core/file_sys/savedata_factory.h" #include "citron/compatibility_list.h" -#include "common/fs/path_util.h" -#include "core/hle/service/acc/profile_manager.h" #include "citron/game_list.h" #include "citron/game_list_p.h" #include "citron/game_list_worker.h" #include "citron/main.h" #include "citron/uisettings.h" #include "citron/util/controller_navigation.h" +#include "common/common_types.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/file_sys/patch_manager.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/savedata_factory.h" +#include "core/hle/service/acc/profile_manager.h" // A helper struct to cleanly pass game data struct SurpriseGame { @@ -84,7 +82,9 @@ public: update(); } - qreal getScrollOffset() const { return m_scroll_offset; } + qreal getScrollOffset() const { + return m_scroll_offset; + } void setScrollOffset(qreal offset) { m_scroll_offset = offset; update(); @@ -92,7 +92,8 @@ public: protected: void paintEvent(QPaintEvent* event) override { - if (m_games.isEmpty()) return; + if (m_games.isEmpty()) + return; QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); @@ -108,7 +109,8 @@ protected: painter.fillRect(widget_center_x - 2, 0, 4, height(), highlight_color); for (int i = 0; i < m_games.size(); ++i) { - const qreal icon_x_position = (widget_center_x - icon_size / 2) + (i * total_slot_width) - m_scroll_offset; + const qreal icon_x_position = + (widget_center_x - icon_size / 2) + (i * total_slot_width) - m_scroll_offset; const int draw_x = static_cast(icon_x_position); const int draw_y = widget_center_y - (icon_size / 2); @@ -139,7 +141,8 @@ class SurpriseMeDialog : public QDialog { public: explicit SurpriseMeDialog(QVector games, QWidget* parent = nullptr) - : QDialog(parent), m_available_games(games), m_last_choice({QString(), QString(), 0, QPixmap()}) { + : QDialog(parent), m_available_games(games), + m_last_choice({QString(), QString(), 0, QPixmap()}) { setWindowTitle(tr("Surprise Me!")); setModal(true); setFixedSize(540, 280); @@ -189,7 +192,9 @@ public: QTimer::singleShot(100, this, &SurpriseMeDialog::startRoll); } - const SurpriseGame& getFinalChoice() const { return m_last_choice; } + const SurpriseGame& getFinalChoice() const { + return m_last_choice; + } private slots: void startRoll() { @@ -214,9 +219,11 @@ private slots: QVector reel; if (!m_available_games.isEmpty()) { std::uniform_int_distribution<> filler_distrib(0, m_available_games.size() - 1); - for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen))); + for (int i = 0; i < 20; ++i) + reel.push_back(m_available_games.at(filler_distrib(gen))); reel.push_back(winner); - for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen))); + for (int i = 0; i < 20; ++i) + reel.push_back(m_available_games.at(filler_distrib(gen))); } else { reel.push_back(winner); } @@ -254,7 +261,9 @@ private slots: } } - void onLaunch() { accept(); } + void onLaunch() { + accept(); + } private: QVector m_available_games; @@ -267,7 +276,8 @@ private: }; // Static helper for Save Detection -static QString GetDetectedEmulatorName(const QString& path, u64 program_id, const QString& citron_nand_base) { +static QString GetDetectedEmulatorName(const QString& path, u64 program_id, + const QString& citron_nand_base) { QString abs_path = QDir(path).absolutePath(); QString citron_abs_base = QDir(citron_nand_base).absolutePath(); QString tid_str = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')); @@ -279,21 +289,29 @@ static QString GetDetectedEmulatorName(const QString& path, u64 program_id, cons // Ryujinx if (abs_path.contains(QStringLiteral("bis/user/save"), Qt::CaseInsensitive)) { - if (abs_path.contains(QStringLiteral("ryubing"), Qt::CaseInsensitive)) return QStringLiteral("Ryubing"); - if (abs_path.contains(QStringLiteral("ryujinx"), Qt::CaseInsensitive)) return QStringLiteral("Ryujinx"); + if (abs_path.contains(QStringLiteral("ryubing"), Qt::CaseInsensitive)) + return QStringLiteral("Ryubing"); + if (abs_path.contains(QStringLiteral("ryujinx"), Qt::CaseInsensitive)) + return QStringLiteral("Ryujinx"); // Fallback if it's a generic Ryujinx-structure folder - return abs_path.contains(tid_str, Qt::CaseInsensitive) ? QStringLiteral("Ryujinx/Ryubing") : QStringLiteral("Ryujinx/Ryubing (Manual Slot)"); + return abs_path.contains(tid_str, Qt::CaseInsensitive) + ? QStringLiteral("Ryujinx/Ryubing") + : QStringLiteral("Ryujinx/Ryubing (Manual Slot)"); } // Fork if (abs_path.contains(QStringLiteral("nand/user/save"), Qt::CaseInsensitive) || abs_path.contains(QStringLiteral("nand/system/Containers"), Qt::CaseInsensitive)) { - if (abs_path.contains(QStringLiteral("eden"), Qt::CaseInsensitive)) return QStringLiteral("Eden"); - if (abs_path.contains(QStringLiteral("suyu"), Qt::CaseInsensitive)) return QStringLiteral("Suyu"); - if (abs_path.contains(QStringLiteral("sudachi"), Qt::CaseInsensitive)) return QStringLiteral("Sudachi"); - if (abs_path.contains(QStringLiteral("yuzu"), Qt::CaseInsensitive)) return QStringLiteral("Yuzu"); + if (abs_path.contains(QStringLiteral("eden"), Qt::CaseInsensitive)) + return QStringLiteral("Eden"); + if (abs_path.contains(QStringLiteral("suyu"), Qt::CaseInsensitive)) + return QStringLiteral("Suyu"); + if (abs_path.contains(QStringLiteral("sudachi"), Qt::CaseInsensitive)) + return QStringLiteral("Sudachi"); + if (abs_path.contains(QStringLiteral("yuzu"), Qt::CaseInsensitive)) + return QStringLiteral("Yuzu"); return QStringLiteral("another emulator"); } @@ -302,7 +320,7 @@ static QString GetDetectedEmulatorName(const QString& path, u64 program_id, cons } GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent) -: QObject(parent), gamelist{gamelist_} {} + : QObject(parent), gamelist{gamelist_} {} // EventFilter in order to process systemkeys while editing the searchfield bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* event) { @@ -317,29 +335,29 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve // If no function key changes the searchfield's text the filter doesn't need to get reloaded if (edit_filter_text == edit_filter_text_old) { switch (keyEvent->key()) { - case Qt::Key_Escape: { - if (edit_filter_text_old.isEmpty()) { - return QObject::eventFilter(obj, event); - } else { - gamelist->search_field->edit_filter->clear(); - edit_filter_text.clear(); - } - break; - } - case Qt::Key_Return: - case Qt::Key_Enter: { - if (gamelist->search_field->visible == 1) { - const QString file_path = gamelist->GetLastFilterResultItem(); - gamelist->search_field->edit_filter->clear(); - edit_filter_text.clear(); - emit gamelist->GameChosen(file_path); - } else { - return QObject::eventFilter(obj, event); - } - break; - } - default: + case Qt::Key_Escape: { + if (edit_filter_text_old.isEmpty()) { return QObject::eventFilter(obj, event); + } else { + gamelist->search_field->edit_filter->clear(); + edit_filter_text.clear(); + } + break; + } + case Qt::Key_Return: + case Qt::Key_Enter: { + if (gamelist->search_field->visible == 1) { + const QString file_path = gamelist->GetLastFilterResultItem(); + gamelist->search_field->edit_filter->clear(); + edit_filter_text.clear(); + emit gamelist->GameChosen(file_path); + } else { + return QObject::eventFilter(obj, event); + } + break; + } + default: + return QObject::eventFilter(obj, event); } } edit_filter_text_old = edit_filter_text; @@ -398,8 +416,8 @@ GameListSearchField::GameListSearchField(GameList* parent) : QWidget{parent} { button_filter_close = new QToolButton(this); button_filter_close->setText(QStringLiteral("X")); button_filter_close->setCursor(Qt::ArrowCursor); - button_filter_close->setStyleSheet( - QStringLiteral("QToolButton{ border: 1px solid palette(mid); border-radius: 4px; padding: 4px 8px; color: " + button_filter_close->setStyleSheet(QStringLiteral( + "QToolButton{ border: 1px solid palette(mid); border-radius: 4px; padding: 4px 8px; color: " "palette(text); font-weight: bold; background: palette(button); }" "QToolButton:hover{ border: 1px solid palette(highlight); color: " "palette(highlighted-text); background: palette(highlight)}")); @@ -422,8 +440,8 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput) void GameList::OnItemExpanded(const QModelIndex& item) { const auto type = item.data(GameListItem::TypeRole).value(); const bool is_dir = type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir || - type == GameListItemType::UserNandDir || - type == GameListItemType::SysNandDir; + type == GameListItemType::UserNandDir || + type == GameListItemType::SysNandDir; const bool is_fave = type == GameListItemType::Favorites; if (!is_dir && !is_fave) { return; @@ -469,23 +487,33 @@ void GameList::FilterGridView(const QString& filter_text) { int total_count = 0; for (int i = 0; i < hierarchical_model->rowCount(); ++i) { QStandardItem* folder = hierarchical_model->item(i, 0); - if (!folder || folder->data(GameListItem::TypeRole).value() == GameListItemType::AddDir) { + if (!folder || folder->data(GameListItem::TypeRole).value() == + GameListItemType::AddDir) { continue; } for (int j = 0; j < folder->rowCount(); ++j) { QStandardItem* game_item = folder->child(j, 0); - if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) continue; + if (!game_item || game_item->data(GameListItem::TypeRole).value() != + GameListItemType::Game) + continue; total_count++; const QString full_path = game_item->data(GameListItemPath::FullPathRole).toString(); bool should_show = !UISettings::values.hidden_paths.contains(full_path); if (should_show && !filter_text.isEmpty()) { - const QString file_title = game_item->data(GameListItemPath::TitleRole).toString().toLower(); - const auto program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong(); - const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')); - const QString file_name = full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + QLatin1Char{' '} + file_title; - should_show = ContainsAllWords(file_name, filter_text) || (file_program_id.size() == 16 && file_program_id.contains(filter_text)); + const QString file_title = + game_item->data(GameListItemPath::TitleRole).toString().toLower(); + const auto program_id = + game_item->data(GameListItemPath::ProgramIdRole).toULongLong(); + const QString file_program_id = + QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')); + const QString file_name = + full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + + QLatin1Char{' '} + file_title; + should_show = + ContainsAllWords(file_name, filter_text) || + (file_program_id.size() == 16 && file_program_id.contains(filter_text)); } if (should_show) { @@ -515,10 +543,11 @@ void GameList::FilterGridView(const QString& filter_text) { if (icon_data.isValid() && icon_data.canConvert()) { QPixmap pixmap = icon_data.value(); if (!pixmap.isNull()) { - #ifdef __linux__ - QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +#ifdef __linux__ + QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); item->setData(scaled, Qt::DecorationRole); - #else +#else QPixmap rounded(icon_size, icon_size); rounded.fill(Qt::transparent); QPainter painter(&rounded); @@ -527,10 +556,11 @@ void GameList::FilterGridView(const QString& filter_text) { QPainterPath path; path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius); painter.setClipPath(path); - QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); painter.drawPixmap(0, 0, scaled); item->setData(rounded, Qt::DecorationRole); - #endif +#endif } } } @@ -542,16 +572,20 @@ void GameList::FilterTreeView(const QString& filter_text) { int visible_count = 0; int total_count = 0; - tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), filter_text.isEmpty() ? (UISettings::values.favorited_ids.size() == 0) : true); + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + filter_text.isEmpty() ? (UISettings::values.favorited_ids.size() == 0) + : true); for (int i = 0; i < item_model->rowCount(); ++i) { QStandardItem* folder = item_model->item(i, 0); - if (!folder) continue; + if (!folder) + continue; const QModelIndex folder_index = folder->index(); for (int j = 0; j < folder->rowCount(); ++j) { const QStandardItem* child = folder->child(j, 0); - if (!child) continue; + if (!child) + continue; total_count++; const QString full_path = child->data(GameListItemPath::FullPathRole).toString(); @@ -560,10 +594,16 @@ void GameList::FilterTreeView(const QString& filter_text) { if (!filter_text.isEmpty()) { const auto program_id = child->data(GameListItemPath::ProgramIdRole).toULongLong(); - const QString file_title = child->data(GameListItemPath::TitleRole).toString().toLower(); - const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')); - const QString file_name = full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + QLatin1Char{' '} + file_title; - matches_filter = ContainsAllWords(file_name, filter_text) || (file_program_id.size() == 16 && file_program_id.contains(filter_text)); + const QString file_title = + child->data(GameListItemPath::TitleRole).toString().toLower(); + const QString file_program_id = + QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')); + const QString file_name = + full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + + QLatin1Char{' '} + file_title; + matches_filter = + ContainsAllWords(file_name, filter_text) || + (file_program_id.size() == 16 && file_program_id.contains(filter_text)); } if (!is_hidden_by_user && matches_filter) { @@ -582,29 +622,55 @@ void GameList::OnUpdateThemedIcons() { QStandardItem* child = item_model->invisibleRootItem()->child(i); const int icon_size = UISettings::values.folder_icon_size.GetValue(); switch (child->data(GameListItem::TypeRole).value()) { - case GameListItemType::SdmcDir: - child->setData(QIcon::fromTheme(QStringLiteral("sd_card")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - case GameListItemType::UserNandDir: - child->setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - case GameListItemType::SysNandDir: - child->setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - case GameListItemType::CustomDir: { - const UISettings::GameDir& game_dir = UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()]; - const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path)) ? QStringLiteral("folder") : QStringLiteral("bad_folder"); - child->setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - } - case GameListItemType::AddDir: - child->setData(QIcon::fromTheme(QStringLiteral("list-add")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - case GameListItemType::Favorites: - child->setData(QIcon::fromTheme(QStringLiteral("star")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); - break; - default: - break; + case GameListItemType::SdmcDir: + child->setData( + QIcon::fromTheme(QStringLiteral("sd_card")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::UserNandDir: + child->setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::SysNandDir: + child->setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::CustomDir: { + const UISettings::GameDir& game_dir = + UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()]; + const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path)) + ? QStringLiteral("folder") + : QStringLiteral("bad_folder"); + child->setData( + QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( + icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + } + case GameListItemType::AddDir: + child->setData( + QIcon::fromTheme(QStringLiteral("list-add")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::Favorites: + child->setData( + QIcon::fromTheme(QStringLiteral("star")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + default: + break; } } } @@ -613,11 +679,12 @@ void GameList::OnFilterCloseClicked() { main_window->filterBarSetChecked(false); } -GameList::GameList(std::shared_ptr vfs_, FileSys::ManualContentProvider* provider_, +GameList::GameList(std::shared_ptr vfs_, + FileSys::ManualContentProvider* provider_, PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, GMainWindow* parent) -: QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, -play_time_manager{play_time_manager_}, system{system_} { + : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, + play_time_manager{play_time_manager_}, system{system_} { watcher = new QFileSystemWatcher(this); connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory); @@ -649,7 +716,9 @@ play_time_manager{play_time_manager_}, system{system_} { list_view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); list_view->setEditTriggers(QAbstractItemView::NoEditTriggers); list_view->setContextMenuPolicy(Qt::CustomContextMenu); - list_view->setStyleSheet(QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { text-align: center; padding: 5px; }")); + list_view->setStyleSheet( + QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { " + "text-align: center; padding: 5px; }")); list_view->setGridSize(QSize(140, 160)); list_view->setSpacing(10); list_view->setWordWrap(true); @@ -671,27 +740,29 @@ play_time_manager{play_time_manager_}, system{system_} { connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded); connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded); // Sync sort button with Name column header sort order - connect(tree_view->header(), &QHeaderView::sortIndicatorChanged, [this](int logicalIndex, Qt::SortOrder order) { - if (logicalIndex == COLUMN_NAME) { - current_sort_order = order; - UpdateSortButtonIcon(); - } - }); + connect(tree_view->header(), &QHeaderView::sortIndicatorChanged, + [this](int logicalIndex, Qt::SortOrder order) { + if (logicalIndex == COLUMN_NAME) { + current_sort_order = order; + UpdateSortButtonIcon(); + } + }); connect(list_view, &QListView::activated, this, &GameList::ValidateEntry); connect(list_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu); - connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, [this](Qt::Key key) { - if (system.IsPoweredOn() || !this->isActiveWindow()) { - return; - } - QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); - if (tree_view->isVisible() && tree_view->model()) { - QCoreApplication::postEvent(tree_view, event); - } - if (list_view->isVisible() && list_view->model()) { - QKeyEvent* list_event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); - QCoreApplication::postEvent(list_view, list_event); - } - }); + connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, + [this](Qt::Key key) { + if (system.IsPoweredOn() || !this->isActiveWindow()) { + return; + } + QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); + if (tree_view->isVisible() && tree_view->model()) { + QCoreApplication::postEvent(tree_view, event); + } + if (list_view->isVisible() && list_view->model()) { + QKeyEvent* list_event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); + QCoreApplication::postEvent(list_view, list_event); + } + }); qRegisterMetaType>("QList"); qRegisterMetaType>>("std::map>"); @@ -718,20 +789,18 @@ play_time_manager{play_time_manager_}, system{system_} { btn_list_view->setAutoRaise(true); btn_list_view->setIconSize(QSize(16, 16)); btn_list_view->setFixedSize(32, 32); - btn_list_view->setStyleSheet(QStringLiteral( - "QToolButton {" - " border: 1px solid palette(mid);" - " border-radius: 4px;" - " background: palette(button);" - "}" - "QToolButton:hover {" - " background: palette(light);" - "}" - "QToolButton:checked {" - " background: palette(highlight);" - " border-color: palette(highlight);" - "}" - )); + btn_list_view->setStyleSheet(QStringLiteral("QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}" + "QToolButton:checked {" + " background: palette(highlight);" + " border-color: palette(highlight);" + "}")); connect(btn_list_view, &QToolButton::clicked, [this]() { SetViewMode(false); btn_list_view->setChecked(true); @@ -754,20 +823,18 @@ play_time_manager{play_time_manager_}, system{system_} { btn_grid_view->setAutoRaise(true); btn_grid_view->setIconSize(QSize(16, 16)); btn_grid_view->setFixedSize(32, 32); - btn_grid_view->setStyleSheet(QStringLiteral( - "QToolButton {" - " border: 1px solid palette(mid);" - " border-radius: 4px;" - " background: palette(button);" - "}" - "QToolButton:hover {" - " background: palette(light);" - "}" - "QToolButton:checked {" - " background: palette(highlight);" - " border-color: palette(highlight);" - "}" - )); + btn_grid_view->setStyleSheet(QStringLiteral("QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}" + "QToolButton:checked {" + " background: palette(highlight);" + " border-color: palette(highlight);" + "}")); connect(btn_grid_view, &QToolButton::clicked, [this]() { SetViewMode(true); btn_list_view->setChecked(false); @@ -782,94 +849,101 @@ play_time_manager{play_time_manager_}, system{system_} { slider_title_size->setToolTip(tr("Game Icon Size")); slider_title_size->setMaximumWidth(120); slider_title_size->setMinimumWidth(120); - slider_title_size->setStyleSheet(QStringLiteral( - "QSlider::groove:horizontal {" - " border: 1px solid palette(mid);" - " height: 4px;" - " background: palette(base);" - " border-radius: 2px;" - "}" - "QSlider::handle:horizontal {" - " background: palette(button);" - " border: 1px solid palette(mid);" - " width: 12px;" - " height: 12px;" - " margin: -4px 0;" - " border-radius: 6px;" - "}" - "QSlider::handle:horizontal:hover {" - " background: palette(light);" - "}" - )); + slider_title_size->setStyleSheet(QStringLiteral("QSlider::groove:horizontal {" + " border: 1px solid palette(mid);" + " height: 4px;" + " background: palette(base);" + " border-radius: 2px;" + "}" + "QSlider::handle:horizontal {" + " background: palette(button);" + " border: 1px solid palette(mid);" + " width: 12px;" + " height: 12px;" + " margin: -4px 0;" + " border-radius: 6px;" + "}" + "QSlider::handle:horizontal:hover {" + " background: palette(light);" + "}")); connect(slider_title_size, &QSlider::valueChanged, [this](int value) { - // Update title font size in tree view - QFont font = tree_view->font(); - font.setPointSize(qBound(8, value / 8, 24)); - tree_view->setFont(font); + // Update title font size in tree view + QFont font = tree_view->font(); + font.setPointSize(qBound(8, value / 8, 24)); + tree_view->setFont(font); #ifndef __linux__ - // On non-Linux platforms, also update game icon size and repaint grid view - UISettings::values.game_icon_size.SetValue(static_cast(value)); - if (list_view->isVisible()) { - QAbstractItemModel* current_model = list_view->model(); - if (current_model && current_model != item_model) { - QStandardItemModel* flat_model = qobject_cast(current_model); - if (flat_model) { - const u32 icon_size = static_cast(value); - list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); - int scroll_position = list_view->verticalScrollBar()->value(); - QModelIndex current_index = list_view->currentIndex(); + // On non-Linux platforms, also update game icon size and repaint grid view + UISettings::values.game_icon_size.SetValue(static_cast(value)); + if (list_view->isVisible()) { + QAbstractItemModel* current_model = list_view->model(); + if (current_model && current_model != item_model) { + QStandardItemModel* flat_model = qobject_cast(current_model); + if (flat_model) { + const u32 icon_size = static_cast(value); + list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); + int scroll_position = list_view->verticalScrollBar()->value(); + QModelIndex current_index = list_view->currentIndex(); - for (int i = 0; i < flat_model->rowCount(); ++i) { - QStandardItem* item = flat_model->item(i); - if (item) { - u64 program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong(); - QStandardItem* original_item = nullptr; - for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) { - QStandardItem* folder = item_model->item(folder_idx, 0); - if (!folder) continue; - for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) { - QStandardItem* game = folder->child(game_idx, 0); - if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { - original_item = game; - break; + for (int i = 0; i < flat_model->rowCount(); ++i) { + QStandardItem* item = flat_model->item(i); + if (item) { + u64 program_id = + item->data(GameListItemPath::ProgramIdRole).toULongLong(); + QStandardItem* original_item = nullptr; + for (int folder_idx = 0; folder_idx < item_model->rowCount(); + ++folder_idx) { + QStandardItem* folder = item_model->item(folder_idx, 0); + if (!folder) + continue; + for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) { + QStandardItem* game = folder->child(game_idx, 0); + if (game && + game->data(GameListItemPath::ProgramIdRole).toULongLong() == + program_id) { + original_item = game; + break; + } } + if (original_item) + break; } - if (original_item) break; - } - if (original_item) { - QVariant orig_icon_data = original_item->data(Qt::DecorationRole); - if (orig_icon_data.isValid() && orig_icon_data.type() == QVariant::Pixmap) { - QPixmap orig_pixmap = orig_icon_data.value(); - QPixmap rounded(icon_size, icon_size); - rounded.fill(Qt::transparent); - QPainter painter(&rounded); - painter.setRenderHint(QPainter::Antialiasing); - const int radius = icon_size / 8; - QPainterPath path; - path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius); - painter.setClipPath(path); - QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - painter.drawPixmap(0, 0, scaled); - item->setData(rounded, Qt::DecorationRole); + if (original_item) { + QVariant orig_icon_data = original_item->data(Qt::DecorationRole); + if (orig_icon_data.isValid() && + orig_icon_data.type() == QVariant::Pixmap) { + QPixmap orig_pixmap = orig_icon_data.value(); + QPixmap rounded(icon_size, icon_size); + rounded.fill(Qt::transparent); + QPainter painter(&rounded); + painter.setRenderHint(QPainter::Antialiasing); + const int radius = icon_size / 8; + QPainterPath path; + path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius); + painter.setClipPath(path); + QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + painter.drawPixmap(0, 0, scaled); + item->setData(rounded, Qt::DecorationRole); + } } } } + if (scroll_position >= 0) { + list_view->verticalScrollBar()->setValue(scroll_position); + } + if (current_index.isValid() && current_index.row() < flat_model->rowCount()) { + list_view->setCurrentIndex(flat_model->index(current_index.row(), 0)); + } } - if (scroll_position >= 0) { - list_view->verticalScrollBar()->setValue(scroll_position); - } - if (current_index.isValid() && current_index.row() < flat_model->rowCount()) { - list_view->setCurrentIndex(flat_model->index(current_index.row(), 0)); - } + } else { + PopulateGridView(); } - } else { - PopulateGridView(); } - } #endif -}); + }); // A-Z sort button - positioned after slider btn_sort_az = new QToolButton(toolbar); @@ -878,16 +952,14 @@ play_time_manager{play_time_manager_}, system{system_} { btn_sort_az->setAutoRaise(true); btn_sort_az->setIconSize(QSize(16, 16)); btn_sort_az->setFixedSize(32, 32); - btn_sort_az->setStyleSheet(QStringLiteral( - "QToolButton {" - " border: 1px solid palette(mid);" - " border-radius: 4px;" - " background: palette(button);" - "}" - "QToolButton:hover {" - " background: palette(light);" - "}" - )); + btn_sort_az->setStyleSheet(QStringLiteral("QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}")); connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder); // Surprise Me button - positioned after sort button @@ -908,16 +980,14 @@ play_time_manager{play_time_manager_}, system{system_} { btn_surprise_me->setAutoRaise(true); btn_surprise_me->setIconSize(QSize(16, 16)); btn_surprise_me->setFixedSize(32, 32); - btn_surprise_me->setStyleSheet(QStringLiteral( - "QToolButton {" - " border: 1px solid palette(mid);" - " border-radius: 4px;" - " background: palette(button);" - "}" - "QToolButton:hover {" - " background: palette(light);" - "}" - )); + btn_surprise_me->setStyleSheet(QStringLiteral("QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}")); connect(btn_surprise_me, &QToolButton::clicked, this, &GameList::onSurpriseMeClicked); // Create progress bar @@ -925,10 +995,9 @@ play_time_manager{play_time_manager_}, system{system_} { progress_bar->setVisible(false); progress_bar->setFixedHeight(4); progress_bar->setTextVisible(false); - progress_bar->setStyleSheet(QStringLiteral( - "QProgressBar { border: none; background: transparent; } " - "QProgressBar::chunk { background-color: #0078d4; }" - )); + progress_bar->setStyleSheet( + QStringLiteral("QProgressBar { border: none; background: transparent; } " + "QProgressBar::chunk { background-color: #0078d4; }")); // Add widgets to toolbar toolbar_layout->addWidget(btn_list_view); @@ -958,7 +1027,8 @@ play_time_manager{play_time_manager_}, system{system_} { connect(&config_update_timer, &QTimer::timeout, this, &GameList::UpdateOnlineStatus); // This connection handles live updates when OK/Apply is clicked in the config window. - connect(main_window, &GMainWindow::ConfigurationSaved, this, &GameList::UpdateAccentColorStyles); + connect(main_window, &GMainWindow::ConfigurationSaved, this, + &GameList::UpdateAccentColorStyles); network_manager = new QNetworkAccessManager(this); @@ -1012,7 +1082,9 @@ void GameList::WorkerEvent() { void GameList::AddDirEntry(GameListDir* entry_items) { item_model->invisibleRootItem()->appendRow(entry_items); - tree_view->setExpanded(entry_items->index(), UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()].expanded); + tree_view->setExpanded( + entry_items->index(), + UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()].expanded); } void GameList::AddEntry(const QList& entry_items, GameListDir* parent) { @@ -1027,10 +1099,11 @@ void GameList::UpdateOnlineStatus() { // A watcher gets the result back on the main thread safely auto online_status_watcher = new QFutureWatcher>>(this); - connect(online_status_watcher, &QFutureWatcher>>::finished, this, [this, online_status_watcher]() { - OnOnlineStatusUpdated(online_status_watcher->result()); - online_status_watcher->deleteLater(); // Clean up the watcher - }); + connect(online_status_watcher, &QFutureWatcher>>::finished, + this, [this, online_status_watcher]() { + OnOnlineStatusUpdated(online_status_watcher->result()); + online_status_watcher->deleteLater(); // Clean up the watcher + }); // Run the blocking network call in a background thread using QtConcurrent QFuture>> future = QtConcurrent::run([session]() { @@ -1061,11 +1134,14 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o for (int i = 0; i < item_model->rowCount(); ++i) { QStandardItem* folder = item_model->item(i, 0); - if (!folder) continue; + if (!folder) + continue; for (int j = 0; j < folder->rowCount(); ++j) { QStandardItem* game_item = folder->child(j, COLUMN_NAME); - if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) continue; + if (!game_item || game_item->data(GameListItem::TypeRole).value() != + GameListItemType::Game) + continue; u64 program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong(); QString online_text = QStringLiteral("N/A"); @@ -1073,7 +1149,8 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o auto it_stats = online_stats.find(program_id); if (it_stats != online_stats.end()) { const auto& stats = it_stats->second; - online_text = QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second); + online_text = + QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second); } QStandardItem* online_item = folder->child(j, COLUMN_ONLINE); @@ -1088,13 +1165,15 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o void GameList::StartLaunchAnimation(const QModelIndex& item) { const QString file_path = item.data(GameListItemPath::FullPathRole).toString(); - if (file_path.isEmpty()) return; + if (file_path.isEmpty()) + return; u64 program_id = item.data(GameListItemPath::ProgramIdRole).toULongLong(); QStandardItem* original_item = nullptr; for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) { QStandardItem* folder = item_model->item(folder_idx, 0); - if (!folder) continue; + if (!folder) + continue; for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) { QStandardItem* game = folder->child(game_idx, 0); if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { @@ -1102,7 +1181,8 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) { break; } } - if (original_item) break; + if (original_item) + break; } QPixmap icon; @@ -1189,11 +1269,12 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) { // When the icon animation finishes, launch the game and clean up. // The black overlay will remain until OnEmulationEnded is called. - connect(main_group, &QSequentialAnimationGroup::finished, this, [this, file_path, title_id, animation_label]() { - search_field->clear(); - emit GameChosen(file_path, title_id); - animation_label->deleteLater(); - }); + connect(main_group, &QSequentialAnimationGroup::finished, this, + [this, file_path, title_id, animation_label]() { + search_field->clear(); + emit GameChosen(file_path, title_id); + animation_label->deleteLater(); + }); main_group->start(QAbstractAnimation::DeleteWhenStopped); } @@ -1201,53 +1282,58 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) { void GameList::ValidateEntry(const QModelIndex& item) { const auto selected = item.sibling(item.row(), 0); switch (selected.data(GameListItem::TypeRole).value()) { - case GameListItemType::Game: { - const QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); - if (file_path.isEmpty()) return; - const QFileInfo file_info(file_path); - if (!file_info.exists()) return; + case GameListItemType::Game: { + const QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); + if (file_path.isEmpty()) + return; + const QFileInfo file_info(file_path); + if (!file_info.exists()) + return; - // If the entry is a directory (e.g., for homebrew), launch it directly without animation. - if (file_info.isDir()) { - const QDir dir{file_path}; - const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); - if (matching_main.size() == 1) { - emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); - } - return; // Exit here for directories + // If the entry is a directory (e.g., for homebrew), launch it directly without animation. + if (file_info.isDir()) { + const QDir dir{file_path}; + const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); + if (matching_main.size() == 1) { + emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); } - - // If it's a standard game file, trigger the new launch animation. - // The animation function will handle emitting GameChosen when it's finished. - StartLaunchAnimation(selected); - break; + return; // Exit here for directories } - case GameListItemType::AddDir: - emit AddDirectory(); - if (UISettings::values.prompt_for_autoloader) { - QMessageBox msg_box(this); - msg_box.setWindowTitle(tr("Autoloader")); - msg_box.setText( - tr("Would you like to use the Autoloader to install all Updates/DLC within your game directories?\n\n" - "If not now, you can always go to Emulation -> Configure -> Filesystem in order to use this feature. Also, if you have multiple update files for a single game, you can use the Update Manager " - "in File -> Install Updates with Update Manager.")); - msg_box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - QCheckBox* check_box = new QCheckBox(tr("Do not ask me again")); - msg_box.setCheckBox(check_box); + // If it's a standard game file, trigger the new launch animation. + // The animation function will handle emitting GameChosen when it's finished. + StartLaunchAnimation(selected); + break; + } + case GameListItemType::AddDir: + emit AddDirectory(); - if (msg_box.exec() == QMessageBox::Yes) { - emit RunAutoloaderRequested(); - } + if (UISettings::values.prompt_for_autoloader) { + QMessageBox msg_box(this); + msg_box.setWindowTitle(tr("Autoloader")); + msg_box.setText( + tr("Would you like to use the Autoloader to install all Updates/DLC within your " + "game directories?\n\n" + "If not now, you can always go to Emulation -> Configure -> Filesystem in order " + "to use this feature. Also, if you have multiple update files for a single " + "game, you can use the Update Manager " + "in File -> Install Updates with Update Manager.")); + msg_box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + QCheckBox* check_box = new QCheckBox(tr("Do not ask me again")); + msg_box.setCheckBox(check_box); - if (check_box->isChecked()) { - UISettings::values.prompt_for_autoloader = false; - emit SaveConfig(); - } + if (msg_box.exec() == QMessageBox::Yes) { + emit RunAutoloaderRequested(); } - break; - default: - break; + + if (check_box->isChecked()) { + UISettings::values.prompt_for_autoloader = false; + emit SaveConfig(); + } + } + break; + default: + break; } } @@ -1255,7 +1341,9 @@ bool GameList::IsEmpty() const { for (int i = 0; i < item_model->rowCount(); i++) { const QStandardItem* child = item_model->invisibleRootItem()->child(i); const auto type = static_cast(child->type()); - if (!child->hasChildren() && (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir)) { + if (!child->hasChildren() && + (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || + type == GameListItemType::SysNandDir)) { item_model->invisibleRootItem()->removeRow(child->row()); i--; } @@ -1270,8 +1358,10 @@ void GameList::DonePopulating(const QStringList& watch_list) { emit ShowList(!IsEmpty()); item_model->invisibleRootItem()->appendRow(new GameListAddDir()); item_model->invisibleRootItem()->insertRow(0, new GameListFavorites()); - tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), UISettings::values.favorited_ids.size() == 0); - tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(), UISettings::values.favorites_expanded.GetValue()); + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + UISettings::values.favorited_ids.size() == 0); + tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(), + UISettings::values.favorites_expanded.GetValue()); for (const auto id : UISettings::values.favorited_ids) { AddFavorite(id); } @@ -1295,7 +1385,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { if (children_total > 0) { search_field->setFocus(); } - item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); + item_model->sort(tree_view->header()->sortIndicatorSection(), + tree_view->header()->sortIndicatorOrder()); if (list_view->isVisible()) { // Preserve filter when repopulating QString filter_text = search_field->filterText(); @@ -1304,6 +1395,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { } else { PopulateGridView(); } + } else { + FilterTreeView(search_field->filterText()); } // Only sync if we aren't rebuilding the UI and the game isn't running. @@ -1316,7 +1409,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { LOG_INFO(Frontend, "Mirroring: Startup sync already performed this session. Skipping."); } } else { - LOG_INFO(Frontend, "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating)."); + LOG_INFO(Frontend, + "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating)."); } // Automatically refresh compatibility data from GitHub if enabled @@ -1334,31 +1428,34 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { } else { item = list_view->indexAt(menu_location); } - if (!item.isValid()) return; + if (!item.isValid()) + return; const auto selected = item.sibling(item.row(), 0); QMenu context_menu; switch (selected.data(GameListItem::TypeRole).value()) { - case GameListItemType::Game: { - const u64 program_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong(); - const std::string path = selected.data(GameListItemPath::FullPathRole).toString().toStdString(); - const QString game_name = selected.data(GameListItemPath::TitleRole).toString(); - AddGamePopup(context_menu, program_id, path, game_name); - break; - } - case GameListItemType::CustomDir: - AddPermDirPopup(context_menu, selected); - AddCustomDirPopup(context_menu, selected); - break; - case GameListItemType::SdmcDir: - case GameListItemType::UserNandDir: - case GameListItemType::SysNandDir: - AddPermDirPopup(context_menu, selected); - break; - case GameListItemType::Favorites: - AddFavoritesPopup(context_menu); - break; - default: - break; + case GameListItemType::Game: { + const u64 program_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong(); + const std::string path = + selected.data(GameListItemPath::FullPathRole).toString().toStdString(); + const QString game_name = selected.data(GameListItemPath::TitleRole).toString(); + AddGamePopup(context_menu, program_id, path, game_name); + break; + } + case GameListItemType::CustomDir: + AddPermDirPopup(context_menu, selected); + AddCustomDirPopup(context_menu, selected, + false); // Pass false to skip adding "Show Hidden Games" + break; + case GameListItemType::SdmcDir: + case GameListItemType::UserNandDir: + case GameListItemType::SysNandDir: + AddPermDirPopup(context_menu, selected); + break; + case GameListItemType::Favorites: + AddFavoritesPopup(context_menu); + break; + default: + break; } if (tree_view->isVisible()) { context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); @@ -1367,28 +1464,85 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { } } -void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path_str, const QString& game_name) { +void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path_str, + const QString& game_name) { const QString path = QString::fromStdString(path_str); const bool is_mirrored = Settings::values.mirrored_save_paths.count(program_id); const bool has_custom_path = Settings::values.custom_save_paths.count(program_id); QString mirror_base_path; + auto calculateTotalSize = [](const QString& dirPath) -> qint64 { + qint64 totalSize = 0; + QDirIterator size_it(dirPath, QDirIterator::Subdirectories); + while (size_it.hasNext()) { + size_it.next(); + QFileInfo fileInfo = size_it.fileInfo(); + if (fileInfo.isFile()) { + totalSize += fileInfo.size(); + } + } + return totalSize; + }; + + auto copyWithProgress = [calculateTotalSize](const QString& sourceDir, const QString& destDir, + QWidget* parent) -> bool { + QProgressDialog progress(tr("Moving Save Data..."), QString(), 0, 100, parent); + progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setValue(0); + qint64 totalSize = calculateTotalSize(sourceDir); + qint64 copiedSize = 0; + QDir dir(sourceDir); + if (!dir.exists()) + return false; + QDir dest_dir(destDir); + if (!dest_dir.exists()) + dest_dir.mkpath(QStringLiteral(".")); + QDirIterator dir_iter(sourceDir, QDirIterator::Subdirectories); + while (dir_iter.hasNext()) { + dir_iter.next(); + const QFileInfo file_info = dir_iter.fileInfo(); + const QString relative_path = dir.relativeFilePath(file_info.absoluteFilePath()); + const QString dest_path = QDir(destDir).filePath(relative_path); + if (file_info.isDir()) { + dest_dir.mkpath(dest_path); + } else if (file_info.isFile()) { + if (QFile::exists(dest_path)) + QFile::remove(dest_path); + if (!QFile::copy(file_info.absoluteFilePath(), dest_path)) + return false; + copiedSize += file_info.size(); + if (totalSize > 0) { + progress.setValue(static_cast((copiedSize * 100) / totalSize)); + } + } + QCoreApplication::processEvents(); + } + progress.setValue(100); + return true; + }; + QAction* favorite = context_menu.addAction(tr("Favorite")); QAction* hide_game = context_menu.addAction(tr("Hide Game")); context_menu.addSeparator(); QAction* start_game = context_menu.addAction(tr("Start Game")); - QAction* start_game_global = context_menu.addAction(tr("Start Game without Custom Configuration")); + QAction* start_game_global = + context_menu.addAction(tr("Start Game without Custom Configuration")); context_menu.addSeparator(); QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_nand_location = context_menu.addAction(tr("Open NAND Location")); + QAction* open_file_location = context_menu.addAction(tr("Open File Location")); QAction* set_custom_save_path = context_menu.addAction(tr("Set Custom Save Path")); QAction* remove_custom_save_path = context_menu.addAction(tr("Revert to NAND Save Path")); QAction* disable_mirroring = context_menu.addAction(tr("Disable Mirroring")); QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location")); QMenu* open_sdmc_mod_menu = context_menu.addMenu(tr("Open SDMC Mod Data Location")); - QAction* open_current_game_sdmc = open_sdmc_mod_menu->addAction(tr("Open Current Game Location")); + QAction* open_current_game_sdmc = + open_sdmc_mod_menu->addAction(tr("Open Current Game Location")); QAction* open_full_sdmc = open_sdmc_mod_menu->addAction(tr("Open Full Location")); - QAction* open_transferable_shader_cache = context_menu.addAction(tr("Open Transferable Pipeline Cache")); + QAction* open_transferable_shader_cache = + context_menu.addAction(tr("Open Transferable Pipeline Cache")); context_menu.addSeparator(); QMenu* remove_menu = context_menu.addMenu(tr("Remove")); QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); @@ -1407,11 +1561,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* submit_compat_report = context_menu.addAction(tr("Submit Compatibility Report")); - #if !defined(__APPLE__) +#if !defined(__APPLE__) QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); - QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu")); - #endif + QAction* create_applications_menu_shortcut = + shortcut_menu->addAction(tr("Add to Applications Menu")); +#endif context_menu.addSeparator(); QAction* properties = context_menu.addAction(tr("Properties")); @@ -1428,7 +1583,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri open_save_location->setVisible(program_id != 0); open_nand_location->setVisible(is_mirrored); - open_nand_location->setToolTip(tr("Citron uses your NAND while syncing. If you need to make save data modifications, do so in here.")); + open_nand_location->setToolTip(tr("Citron uses your NAND while syncing. If you need to make " + "save data modifications, do so in here.")); set_custom_save_path->setVisible(program_id != 0 && !is_mirrored); remove_custom_save_path->setVisible(program_id != 0 && has_custom_path); disable_mirroring->setVisible(is_mirrored); @@ -1448,21 +1604,28 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri if (has_global_path) { open_nand_location->setText(tr("Open Global Save Path Location")); - open_nand_location->setToolTip(tr("The global save path is being used as the base for save data mirroring.")); - mirror_base_path = QString::fromStdString(Settings::values.global_custom_save_path.GetValue()); + open_nand_location->setToolTip( + tr("The global save path is being used as the base for save data mirroring.")); + mirror_base_path = + QString::fromStdString(Settings::values.global_custom_save_path.GetValue()); } else { - open_nand_location->setToolTip(tr("Citron's default NAND is being used as the base for save data mirroring.")); - mirror_base_path = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir)); + open_nand_location->setToolTip( + tr("Citron's default NAND is being used as the base for save data mirroring.")); + mirror_base_path = QString::fromStdString( + Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir)); } connect(open_nand_location, &QAction::triggered, [this, program_id, mirror_base_path]() { const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128(); - const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id); - const auto full_save_path = std::filesystem::path(mirror_base_path.toStdString()) / relative_save_path; + const std::string relative_save_path = fmt::format( + "user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id); + const auto full_save_path = + std::filesystem::path(mirror_base_path.toStdString()) / relative_save_path; if (!std::filesystem::exists(full_save_path.parent_path())) { std::filesystem::create_directories(full_save_path.parent_path()); } - QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(full_save_path.string()))); + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(full_save_path.string()))); }); } @@ -1470,58 +1633,21 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); }); connect(hide_game, &QAction::triggered, [this, path]() { ToggleHidden(path); }); - connect(open_save_location, &QAction::triggered, [this, program_id, path_str]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str); }); - - auto calculateTotalSize = [](const QString& dirPath) -> qint64 { - qint64 totalSize = 0; - QDirIterator size_it(dirPath, QDirIterator::Subdirectories); - while (size_it.hasNext()) { - size_it.next(); - QFileInfo fileInfo = size_it.fileInfo(); - if (fileInfo.isFile()) { - totalSize += fileInfo.size(); - } - } - return totalSize; - }; - - auto copyWithProgress = [calculateTotalSize](const QString& sourceDir, const QString& destDir, QWidget* parent) -> bool { - QProgressDialog progress(tr("Moving Save Data..."), QString(), 0, 100, parent); - progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(0); - progress.setValue(0); - qint64 totalSize = calculateTotalSize(sourceDir); - qint64 copiedSize = 0; - QDir dir(sourceDir); - if (!dir.exists()) return false; - QDir dest_dir(destDir); - if (!dest_dir.exists()) dest_dir.mkpath(QStringLiteral(".")); - QDirIterator dir_iter(sourceDir, QDirIterator::Subdirectories); - while (dir_iter.hasNext()) { - dir_iter.next(); - const QFileInfo file_info = dir_iter.fileInfo(); - const QString relative_path = dir.relativeFilePath(file_info.absoluteFilePath()); - const QString dest_path = QDir(destDir).filePath(relative_path); - if (file_info.isDir()) { - dest_dir.mkpath(dest_path); - } else if (file_info.isFile()) { - if (QFile::exists(dest_path)) QFile::remove(dest_path); - if (!QFile::copy(file_info.absoluteFilePath(), dest_path)) return false; - copiedSize += file_info.size(); - if (totalSize > 0) { - progress.setValue(static_cast((copiedSize * 100) / totalSize)); - } - } - QCoreApplication::processEvents(); - } - progress.setValue(100); - return true; - }; + connect(open_file_location, &QAction::triggered, [path_str]() { + const QString qpath = QString::fromStdString(path_str); + const QFileInfo file_info(qpath); + QDesktopServices::openUrl(QUrl::fromLocalFile(file_info.absolutePath())); + }); + connect(open_save_location, &QAction::triggered, + [this, program_id, game_name, copyWithProgress, path_str]() { + emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str); + }); connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() { - const QString new_path = QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location")); - if (new_path.isEmpty()) return; + const QString new_path = + QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location")); + if (new_path.isEmpty()) + return; std::string base_save_path_str; if (Settings::values.global_custom_save_path_enabled.GetValue() && !Settings::values.global_custom_save_path.GetValue().empty()) { @@ -1531,17 +1657,24 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri } const QString base_dir = QString::fromStdString(base_save_path_str); const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128(); - const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id); - const QString internal_save_path = QDir(base_dir).filePath(QString::fromStdString(relative_save_path)); + const std::string relative_save_path = fmt::format( + "user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id); + const QString internal_save_path = + QDir(base_dir).filePath(QString::fromStdString(relative_save_path)); bool mirroring_enabled = false; QString detected_emu = GetDetectedEmulatorName(new_path, program_id, base_dir); if (!detected_emu.isEmpty()) { - QMessageBox::StandardButton mirror_reply = QMessageBox::question(this, tr("Enable Save Mirroring?"), - tr("Citron has detected a %1 save structure.\n\n" - "Would you like to enable 'Intelligent Mirroring'? This will pull the data into Citron's internal save directory " - "(currently set to '%2') and keep both locations synced whenever you play. A backup of your existing Citron data " - "will be created. BE WARNED: Please do not have both emulators open during this process.").arg(detected_emu, base_dir), - QMessageBox::Yes | QMessageBox::No); + QMessageBox::StandardButton mirror_reply = + QMessageBox::question(this, tr("Enable Save Mirroring?"), + tr("Citron has detected a %1 save structure.\n\n" + "Would you like to enable 'Intelligent Mirroring'? This " + "will pull the data into Citron's internal save directory " + "(currently set to '%2') and keep both locations synced " + "whenever you play. A backup of your existing Citron data " + "will be created. BE WARNED: Please do not have both " + "emulators open during this process.") + .arg(detected_emu, base_dir), + QMessageBox::Yes | QMessageBox::No); if (mirror_reply == QMessageBox::Yes) { mirroring_enabled = true; @@ -1550,35 +1683,50 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QDir internal_dir(internal_save_path); if (internal_dir.exists() && !internal_dir.isEmpty()) { if (mirroring_enabled) { - QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss")); - QString backup_path = internal_save_path + QStringLiteral("_mirror_backup_") + timestamp; + QString timestamp = + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss")); + QString backup_path = + internal_save_path + QStringLiteral("_mirror_backup_") + timestamp; QDir().mkpath(QFileInfo(backup_path).absolutePath()); if (QDir().rename(internal_save_path, backup_path)) { - LOG_INFO(Frontend, "Safety: Existing internal data moved to backup: {}", backup_path.toStdString()); + LOG_INFO(Frontend, "Safety: Existing internal data moved to backup: {}", + backup_path.toStdString()); } } else { - QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"), - tr("You have existing save data in your internal save directory. Would you like to move it to the new custom save path?"), + QMessageBox::StandardButton reply = QMessageBox::question( + this, tr("Move Save Data"), + tr("You have existing save data in your internal save directory. Would you " + "like to move it to the new custom save path?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (reply == QMessageBox::Cancel) return; + if (reply == QMessageBox::Cancel) + return; if (reply == QMessageBox::Yes) { - const QString full_dest_path = QDir(new_path).filePath(QString::fromStdString(relative_save_path)); + const QString full_dest_path = + QDir(new_path).filePath(QString::fromStdString(relative_save_path)); if (copyWithProgress(internal_save_path, full_dest_path, this)) { QDir(internal_save_path).removeRecursively(); - QMessageBox::information(this, tr("Success"), tr("Successfully moved save data to the new location.")); + QMessageBox::information( + this, tr("Success"), + tr("Successfully moved save data to the new location.")); } else { - QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details.")); + QMessageBox::warning( + this, tr("Error"), + tr("Failed to move save data. Please see the log for more details.")); } } } } if (mirroring_enabled) { if (copyWithProgress(new_path, internal_save_path, this)) { - Settings::values.mirrored_save_paths.insert_or_assign(program_id, new_path.toStdString()); + Settings::values.mirrored_save_paths.insert_or_assign(program_id, + new_path.toStdString()); Settings::values.custom_save_paths.erase(program_id); - QMessageBox::information(this, tr("Success"), tr("Mirroring established. Your data has been pulled into the internal Citron save directory.")); + QMessageBox::information(this, tr("Success"), + tr("Mirroring established. Your data has been pulled into " + "the internal Citron save directory.")); } else { - QMessageBox::warning(this, tr("Error"), tr("Failed to pull data from the mirror source.")); + QMessageBox::warning(this, tr("Error"), + tr("Failed to pull data from the mirror source.")); return; } } else { @@ -1590,20 +1738,24 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(disable_mirroring, &QAction::triggered, [this, program_id]() { if (QMessageBox::question(this, tr("Disable Mirroring"), - tr("Are you sure you want to disable mirroring for this game?\n\nThe directories will no longer be synced."), + tr("Are you sure you want to disable mirroring for this " + "game?\n\nThe directories will no longer be synced."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { Settings::values.mirrored_save_paths.erase(program_id); emit SaveConfig(); QMessageBox::information(this, tr("Mirroring Disabled"), - tr("Mirroring has been disabled for this game. It will now use the save data from the NAND.")); + tr("Mirroring has been disabled for this game. It will now " + "use the save data from the NAND.")); } }); connect(open_current_game_sdmc, &QAction::triggered, [program_id]() { const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir); - const auto full_path = sdmc_path / "atmosphere" / "contents" / fmt::format("{:016X}", program_id); + const auto full_path = + sdmc_path / "atmosphere" / "contents" / fmt::format("{:016X}", program_id); const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path)); QDir dir(qpath); - if (!dir.exists()) dir.mkpath(QStringLiteral(".")); + if (!dir.exists()) + dir.mkpath(QStringLiteral(".")); QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); }); connect(open_full_sdmc, &QAction::triggered, []() { @@ -1611,35 +1763,68 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri const auto full_path = sdmc_path / "atmosphere" / "contents"; const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path)); QDir dir(qpath); - if (!dir.exists()) dir.mkpath(QStringLiteral(".")); + if (!dir.exists()) + dir.mkpath(QStringLiteral(".")); QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); }); - connect(start_game, &QAction::triggered, [this, path_str]() { emit BootGame(QString::fromStdString(path_str), StartGameType::Normal); }); - connect(start_game_global, &QAction::triggered, [this, path_str]() { emit BootGame(QString::fromStdString(path_str), StartGameType::Global); }); - connect(open_mod_location, &QAction::triggered, [this, program_id, path_str]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path_str); }); - connect(open_transferable_shader_cache, &QAction::triggered, [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); - connect(remove_all_content, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Game); }); - connect(remove_update, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Update); }); - connect(remove_dlc, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::AddOnContent); }); - connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::GlShaderCache, path_str); }); - connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::VkShaderCache, path_str); }); - connect(remove_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::AllShaderCache, path_str); }); - connect(remove_custom_config, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path_str); }); - connect(remove_play_time_data, &QAction::triggered, [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); - connect(remove_cache_storage, &QAction::triggered, [this, program_id, path_str] { emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path_str); }); - connect(dump_romfs, &QAction::triggered, [this, program_id, path_str]() { emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::Normal); }); - connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path_str]() { emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::SDMC); }); - connect(verify_integrity, &QAction::triggered, [this, path_str]() { emit VerifyIntegrityRequested(path_str); }); - connect(copy_tid, &QAction::triggered, [this, program_id]() { emit CopyTIDRequested(program_id); }); + connect(start_game, &QAction::triggered, [this, path_str]() { + emit BootGame(QString::fromStdString(path_str), StartGameType::Normal); + }); + connect(start_game_global, &QAction::triggered, [this, path_str]() { + emit BootGame(QString::fromStdString(path_str), StartGameType::Global); + }); + connect(open_mod_location, &QAction::triggered, [this, program_id, path_str]() { + emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path_str); + }); + connect(open_transferable_shader_cache, &QAction::triggered, + [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); + connect(remove_all_content, &QAction::triggered, [this, program_id]() { + emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Game); + }); + connect(remove_update, &QAction::triggered, [this, program_id]() { + emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Update); + }); + connect(remove_dlc, &QAction::triggered, [this, program_id]() { + emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::AddOnContent); + }); + connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path_str]() { + emit RemoveFileRequested(program_id, GameListRemoveTarget::GlShaderCache, path_str); + }); + connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path_str]() { + emit RemoveFileRequested(program_id, GameListRemoveTarget::VkShaderCache, path_str); + }); + connect(remove_shader_cache, &QAction::triggered, [this, program_id, path_str]() { + emit RemoveFileRequested(program_id, GameListRemoveTarget::AllShaderCache, path_str); + }); + connect(remove_custom_config, &QAction::triggered, [this, program_id, path_str]() { + emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path_str); + }); + connect(remove_play_time_data, &QAction::triggered, + [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); + connect(remove_cache_storage, &QAction::triggered, [this, program_id, path_str] { + emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path_str); + }); + connect(dump_romfs, &QAction::triggered, [this, program_id, path_str]() { + emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::Normal); + }); + connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path_str]() { + emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::SDMC); + }); + connect(verify_integrity, &QAction::triggered, + [this, path_str]() { emit VerifyIntegrityRequested(path_str); }); + connect(copy_tid, &QAction::triggered, + [this, program_id]() { emit CopyTIDRequested(program_id); }); connect(submit_compat_report, &QAction::triggered, [this, program_id, game_name]() { - const auto reply = QMessageBox::question(this, tr("GitHub Account Required"), + const auto reply = QMessageBox::question( + this, tr("GitHub Account Required"), tr("In order to submit a compatibility report, you must have a GitHub account.\n\n" "If you do not have one, this feature will not work. Would you like to proceed?"), QMessageBox::Yes | QMessageBox::No); if (reply != QMessageBox::Yes) { return; } - const QString clean_tid = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')).toUpper(); + const QString clean_tid = + QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')).toUpper(); QUrl url(QStringLiteral("https://github.com/CollectingW/Citron-Compatability/issues/new")); QUrlQuery query; query.addQueryItem(QStringLiteral("template"), QStringLiteral("compat.yml")); @@ -1648,35 +1833,45 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri url.setQuery(query); QDesktopServices::openUrl(url); }); - #if !defined(__APPLE__) - connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path_str]() { emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Desktop); }); - connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path_str]() { emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Applications); }); - #endif - connect(properties, &QAction::triggered, [this, path_str]() { emit OpenPerGameGeneralRequested(path_str); }); +#if !defined(__APPLE__) + connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path_str]() { + emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Desktop); + }); + connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path_str]() { + emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Applications); + }); +#endif + connect(properties, &QAction::triggered, + [this, path_str]() { emit OpenPerGameGeneralRequested(path_str); }); } -void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { - UISettings::GameDir& game_dir = UISettings::values.game_dirs[selected.data(GameListDir::GameDirRole).toInt()]; - QAction* show_hidden = context_menu.addAction(tr("Show Hidden Games")); +void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected, + bool show_hidden_action) { + UISettings::GameDir& game_dir = + UISettings::values.game_dirs[selected.data(GameListDir::GameDirRole).toInt()]; + if (show_hidden_action) { + QAction* show_hidden = context_menu.addAction(tr("Show Hidden Games")); + connect(show_hidden, &QAction::triggered, [this, selected] { + QStandardItem* folder = item_model->itemFromIndex(selected); + bool changed = false; + for (int i = 0; i < folder->rowCount(); ++i) { + const QString path = + folder->child(i)->data(GameListItemPath::FullPathRole).toString(); + if (UISettings::values.hidden_paths.removeOne(path)) { + changed = true; + } + } + if (changed) { + OnTextChanged(search_field->filterText()); + emit SaveConfig(); + } + }); + } context_menu.addSeparator(); QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); deep_scan->setCheckable(true); deep_scan->setChecked(game_dir.deep_scan); - connect(show_hidden, &QAction::triggered, [this, selected] { - QStandardItem* folder = item_model->itemFromIndex(selected); - bool changed = false; - for (int i = 0; i < folder->rowCount(); ++i) { - const QString path = folder->child(i)->data(GameListItemPath::FullPathRole).toString(); - if (UISettings::values.hidden_paths.removeOne(path)) { - changed = true; - } - } - if (changed) { - OnTextChanged(search_field->filterText()); - emit SaveConfig(); - } - }); connect(deep_scan, &QAction::triggered, [this, &game_dir] { game_dir.deep_scan = !game_dir.deep_scan; PopulateAsync(UISettings::values.game_dirs); @@ -1714,23 +1909,32 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { }); connect(move_up, &QAction::triggered, [this, selected, row, game_dir_index] { const int other_index = selected.sibling(row - 1, 0).data(GameListDir::GameDirRole).toInt(); - std::swap(UISettings::values.game_dirs[game_dir_index], UISettings::values.game_dirs[other_index]); + std::swap(UISettings::values.game_dirs[game_dir_index], + UISettings::values.game_dirs[other_index]); item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole); - item_model->setData(selected.sibling(row - 1, 0), QVariant(game_dir_index), GameListDir::GameDirRole); + item_model->setData(selected.sibling(row - 1, 0), QVariant(game_dir_index), + GameListDir::GameDirRole); QList item = item_model->takeRow(row); item_model->invisibleRootItem()->insertRow(row - 1, item); - tree_view->setExpanded(selected.sibling(row - 1, 0), UISettings::values.game_dirs[other_index].expanded); + tree_view->setExpanded(selected.sibling(row - 1, 0), + UISettings::values.game_dirs[other_index].expanded); }); connect(move_down, &QAction::triggered, [this, selected, row, game_dir_index] { const int other_index = selected.sibling(row + 1, 0).data(GameListDir::GameDirRole).toInt(); - std::swap(UISettings::values.game_dirs[game_dir_index], UISettings::values.game_dirs[other_index]); + std::swap(UISettings::values.game_dirs[game_dir_index], + UISettings::values.game_dirs[other_index]); item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole); - item_model->setData(selected.sibling(row + 1, 0), QVariant(game_dir_index), GameListDir::GameDirRole); + item_model->setData(selected.sibling(row + 1, 0), QVariant(game_dir_index), + GameListDir::GameDirRole); const QList item = item_model->takeRow(row); item_model->invisibleRootItem()->insertRow(row + 1, item); - tree_view->setExpanded(selected.sibling(row + 1, 0), UISettings::values.game_dirs[other_index].expanded); + tree_view->setExpanded(selected.sibling(row + 1, 0), + UISettings::values.game_dirs[other_index].expanded); + }); + connect(open_directory_location, &QAction::triggered, [this, game_dir_index] { + emit OpenDirectory( + QString::fromStdString(UISettings::values.game_dirs[game_dir_index].path)); }); - connect(open_directory_location, &QAction::triggered, [this, game_dir_index] { emit OpenDirectory(QString::fromStdString(UISettings::values.game_dirs[game_dir_index].path)); }); } void GameList::AddFavoritesPopup(QMenu& context_menu) { @@ -1749,7 +1953,8 @@ void GameList::LoadCompatibilityList() { compatibility_list.clear(); // Look for a downloaded list in the config directory first - const auto config_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir)); + const auto config_dir = + QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir)); const QString local_path = QDir(config_dir).filePath(QStringLiteral("compatibility_list.json")); QFile compat_list; @@ -1780,7 +1985,8 @@ void GameList::LoadCompatibilityList() { const QString compatibility_key = QStringLiteral("compatibility"); // Match the legacy parser logic - if (!game.contains(compatibility_key)) continue; + if (!game.contains(compatibility_key)) + continue; const int compatibility = game[compatibility_key].toInt(); const QString directory = game[QStringLiteral("directory")].toString(); @@ -1789,10 +1995,12 @@ void GameList::LoadCompatibilityList() { for (const QJsonValue id_ref : ids) { const QJsonObject id_object = id_ref.toObject(); const QString id = id_object[QStringLiteral("id")].toString(); - if (id.isEmpty()) continue; + if (id.isEmpty()) + continue; - compatibility_list.insert_or_assign(id.toUpper().toStdString(), - std::make_pair(QString::number(compatibility), directory)); + compatibility_list.insert_or_assign( + id.toUpper().toStdString(), + std::make_pair(QString::number(compatibility), directory)); } } LOG_INFO(Frontend, "Loaded {} compatibility entries.", compatibility_list.size()); @@ -1850,11 +2058,15 @@ void GameList::PopulateAsync(QVector& game_dirs) { progress_bar->setVisible(true); } - current_worker = std::make_unique(vfs, provider, game_dirs, compatibility_list, play_time_manager, system, main_window->GetMultiplayerState()->GetSession()); - connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, Qt::QueuedConnection); + current_worker = std::make_unique( + vfs, provider, game_dirs, compatibility_list, play_time_manager, system, + main_window->GetMultiplayerState()->GetSession()); + connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, + Qt::QueuedConnection); if (progress_bar) { - connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, &QProgressBar::setValue, Qt::QueuedConnection); + connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, + &QProgressBar::setValue, Qt::QueuedConnection); } QThreadPool::globalInstance()->start(current_worker.get()); @@ -1874,9 +2086,8 @@ void GameList::LoadInterfaceLayout() { } const QStringList GameList::supported_file_extensions = { - QStringLiteral("xci"), QStringLiteral("nsp"), - QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("kip") -}; + QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("nso"), QStringLiteral("nro"), + QStringLiteral("kip")}; void GameList::RefreshGameDirectory() { if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) { @@ -1885,275 +2096,293 @@ void GameList::RefreshGameDirectory() { } } - void GameList::ToggleFavorite(u64 program_id) { - if (!UISettings::values.favorited_ids.contains(program_id)) { - tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), !search_field->filterText().isEmpty()); - UISettings::values.favorited_ids.append(program_id); - AddFavorite(program_id); - item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); +void GameList::ToggleFavorite(u64 program_id) { + if (!UISettings::values.favorited_ids.contains(program_id)) { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + !search_field->filterText().isEmpty()); + UISettings::values.favorited_ids.append(program_id); + AddFavorite(program_id); + item_model->sort(tree_view->header()->sortIndicatorSection(), + tree_view->header()->sortIndicatorOrder()); + } else { + UISettings::values.favorited_ids.removeOne(program_id); + RemoveFavorite(program_id); + if (UISettings::values.favorited_ids.size() == 0) { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); + } + } + if (list_view->isVisible()) { + // Preserve filter when updating favorites + QString filter_text = search_field->filterText(); + if (!filter_text.isEmpty()) { + FilterGridView(filter_text); } else { - UISettings::values.favorited_ids.removeOne(program_id); - RemoveFavorite(program_id); - if (UISettings::values.favorited_ids.size() == 0) { - tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); - } + PopulateGridView(); } - if (list_view->isVisible()) { - // Preserve filter when updating favorites - QString filter_text = search_field->filterText(); - if (!filter_text.isEmpty()) { - FilterGridView(filter_text); - } else { - PopulateGridView(); - } - } - SaveConfig(); } + SaveConfig(); +} - void GameList::AddFavorite(u64 program_id) { - auto* favorites_row = item_model->item(0); - for (int i = 1; i < item_model->rowCount() - 1; i++) { - const auto* folder = item_model->item(i); - for (int j = 0; j < folder->rowCount(); j++) { - if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { - QList list; - for (int k = 0; k < COLUMN_COUNT; k++) { - list.append(folder->child(j, k)->clone()); - } - list[0]->setData(folder->child(j)->data(GameListItem::SortRole), GameListItem::SortRole); - list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString()); - favorites_row->appendRow(list); - return; +void GameList::AddFavorite(u64 program_id) { + auto* favorites_row = item_model->item(0); + for (int i = 1; i < item_model->rowCount() - 1; i++) { + const auto* folder = item_model->item(i); + for (int j = 0; j < folder->rowCount(); j++) { + if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() == + program_id) { + QList list; + for (int k = 0; k < COLUMN_COUNT; k++) { + list.append(folder->child(j, k)->clone()); } - } - } - } - - void GameList::RemoveFavorite(u64 program_id) { - auto* favorites_row = item_model->item(0); - for (int i = 0; i < favorites_row->rowCount(); i++) { - const auto* game = favorites_row->child(i); - if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { - favorites_row->removeRow(i); + list[0]->setData(folder->child(j)->data(GameListItem::SortRole), + GameListItem::SortRole); + list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString()); + favorites_row->appendRow(list); return; } } } +} - GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { - connect(parent, &GMainWindow::UpdateThemedIcons, this, &GameListPlaceholder::onUpdateThemedIcons); - layout = new QVBoxLayout; - image = new QLabel; - text = new QLabel; - layout->setAlignment(Qt::AlignCenter); - image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); - RetranslateUI(); - QFont font = text->font(); - font.setPointSize(20); - text->setFont(font); - text->setAlignment(Qt::AlignHCenter); - image->setAlignment(Qt::AlignHCenter); - layout->addWidget(image); - layout->addWidget(text); - setLayout(layout); - } - - GameListPlaceholder::~GameListPlaceholder() = default; - - void GameListPlaceholder::onUpdateThemedIcons() { - image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); - } - - void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { - emit GameListPlaceholder::AddDirectory(); - } - - void GameListPlaceholder::changeEvent(QEvent* event) { - if (event->type() == QEvent::LanguageChange) { - RetranslateUI(); +void GameList::RemoveFavorite(u64 program_id) { + auto* favorites_row = item_model->item(0); + for (int i = 0; i < favorites_row->rowCount(); i++) { + const auto* game = favorites_row->child(i); + if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { + favorites_row->removeRow(i); + return; } - QWidget::changeEvent(event); } +} - void GameListPlaceholder::RetranslateUI() { - text->setText(tr("Double-click to add a new folder to the game list")); +GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { + connect(parent, &GMainWindow::UpdateThemedIcons, this, + &GameListPlaceholder::onUpdateThemedIcons); + layout = new QVBoxLayout; + image = new QLabel; + text = new QLabel; + layout->setAlignment(Qt::AlignCenter); + image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); + RetranslateUI(); + QFont font = text->font(); + font.setPointSize(20); + text->setFont(font); + text->setAlignment(Qt::AlignHCenter); + image->setAlignment(Qt::AlignHCenter); + layout->addWidget(image); + layout->addWidget(text); + setLayout(layout); +} + +GameListPlaceholder::~GameListPlaceholder() = default; + +void GameListPlaceholder::onUpdateThemedIcons() { + image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); +} + +void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { + emit GameListPlaceholder::AddDirectory(); +} + +void GameListPlaceholder::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) { + RetranslateUI(); } + QWidget::changeEvent(event); +} - void GameList::SetViewMode(bool grid_view) { - if (grid_view) { - // Check if there's an active filter - if so, use FilterGridView instead +void GameListPlaceholder::RetranslateUI() { + text->setText(tr("Double-click to add a new folder to the game list")); +} + +void GameList::SetViewMode(bool grid_view) { + if (grid_view) { + // Check if there's an active filter - if so, use FilterGridView instead + QString filter_text = search_field->filterText(); + if (!filter_text.isEmpty()) { + FilterGridView(filter_text); + } else { + PopulateGridView(); + } + tree_view->setVisible(false); + list_view->setVisible(true); + if (list_view->model() && list_view->model()->rowCount() > 0) { + list_view->setCurrentIndex(list_view->model()->index(0, 0)); + } + } else { + list_view->setVisible(false); + tree_view->setVisible(true); + if (item_model && item_model->rowCount() > 0) { + tree_view->setCurrentIndex(item_model->index(0, 0)); + } + } + // Update button states + if (btn_list_view && btn_grid_view) { + btn_list_view->setChecked(!grid_view); + btn_grid_view->setChecked(grid_view); + } +} + +void GameList::PopulateGridView() { + QStandardItemModel* hierarchical_model = item_model; + if (QAbstractItemModel* old_model = list_view->model()) { + if (old_model != item_model) { + old_model->deleteLater(); + } + } + QStandardItemModel* flat_model = new QStandardItemModel(this); + flat_model->setSortRole(GameListItemPath::SortRole); + for (int i = 0; i < hierarchical_model->rowCount(); ++i) { + QStandardItem* folder = hierarchical_model->item(i, 0); + if (!folder) + continue; + const auto folder_type = folder->data(GameListItem::TypeRole).value(); + if (folder_type == GameListItemType::AddDir) { + continue; + } + for (int j = 0; j < folder->rowCount(); ++j) { + QStandardItem* game_item = folder->child(j, 0); + if (!game_item) + continue; + const auto game_type = + game_item->data(GameListItem::TypeRole).value(); + if (game_type == GameListItemType::Game) { + const QString full_path = + game_item->data(GameListItemPath::FullPathRole).toString(); + if (UISettings::values.hidden_paths.contains(full_path)) { + continue; + } + QStandardItem* cloned_item = game_item->clone(); + QString game_title = game_item->data(GameListItemPath::TitleRole).toString(); + if (game_title.isEmpty()) { + std::string filename; + Common::SplitPath( + game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), + nullptr, &filename, nullptr); + game_title = QString::fromStdString(filename); + } + cloned_item->setText(game_title); + flat_model->appendRow(cloned_item); + } + } + } + list_view->setModel(flat_model); + const u32 icon_size = UISettings::values.game_icon_size.GetValue(); + list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); + // Sort the grid view using current sort order + flat_model->sort(0, current_sort_order); + // Update icon sizes in the model - ensure all icons are consistently sized with rounded corners + for (int i = 0; i < flat_model->rowCount(); ++i) { + QStandardItem* item = flat_model->item(i); + if (item) { + QVariant icon_data = item->data(Qt::DecorationRole); + if (icon_data.isValid() && icon_data.canConvert()) { + QPixmap pixmap = icon_data.value(); + if (!pixmap.isNull()) { +#ifdef __linux__ + // On Linux, use simple scaling to avoid QPainter bugs + QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + item->setData(scaled, Qt::DecorationRole); +#else + // On other platforms, use the QPainter method for rounded corners + QPixmap rounded(icon_size, icon_size); + rounded.fill(Qt::transparent); + + QPainter painter(&rounded); + painter.setRenderHint(QPainter::Antialiasing); + + const int radius = icon_size / 8; + QPainterPath path; + path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius); + painter.setClipPath(path); + + QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + painter.drawPixmap(0, 0, scaled); + + item->setData(rounded, Qt::DecorationRole); +#endif + } + } + } + } +} + +void GameList::ToggleViewMode() { + bool current_grid_view = UISettings::values.game_list_grid_view.GetValue(); + UISettings::values.game_list_grid_view.SetValue(!current_grid_view); + SetViewMode(!current_grid_view); + // Button states are updated in SetViewMode +} + +void GameList::SortAlphabetically() { + if (tree_view->isVisible()) { + // Sort tree view by name column using current sort order + tree_view->header()->setSortIndicator(COLUMN_NAME, current_sort_order); + item_model->sort(COLUMN_NAME, current_sort_order); + } else if (list_view->isVisible()) { + // Sort grid view alphabetically using current sort order + QAbstractItemModel* current_model = list_view->model(); + if (current_model && current_model != item_model) { + // Sort the flat model used by list view (filtered or unfiltered) + QStandardItemModel* flat_model = qobject_cast(current_model); + if (flat_model) { + // Use SortRole for proper alphabetical sorting + flat_model->setSortRole(GameListItemPath::SortRole); + flat_model->sort(0, current_sort_order); + } + } else { + // If using item_model directly, repopulate grid view to apply sort + // Preserve filter if active QString filter_text = search_field->filterText(); if (!filter_text.isEmpty()) { FilterGridView(filter_text); } else { PopulateGridView(); } - tree_view->setVisible(false); - list_view->setVisible(true); - if (list_view->model() && list_view->model()->rowCount() > 0) { - list_view->setCurrentIndex(list_view->model()->index(0, 0)); - } - } else { - list_view->setVisible(false); - tree_view->setVisible(true); - if (item_model && item_model->rowCount() > 0) { - tree_view->setCurrentIndex(item_model->index(0, 0)); - } - } - // Update button states - if (btn_list_view && btn_grid_view) { - btn_list_view->setChecked(!grid_view); - btn_grid_view->setChecked(grid_view); } } + UpdateSortButtonIcon(); +} - void GameList::PopulateGridView() { - QStandardItemModel* hierarchical_model = item_model; - if (QAbstractItemModel* old_model = list_view->model()) { - if (old_model != item_model) { - old_model->deleteLater(); - } +void GameList::ToggleSortOrder() { + // Toggle between ascending and descending, just like clicking the Name column header + current_sort_order = + (current_sort_order == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; + SortAlphabetically(); +} + +void GameList::UpdateSortButtonIcon() { + if (!btn_sort_az) + return; + + QIcon sort_icon; + if (current_sort_order == Qt::AscendingOrder) { + // Ascending (A-Z) - arrow up + sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-ascending")); + if (sort_icon.isNull()) { + sort_icon = QIcon::fromTheme(QStringLiteral("sort-ascending")); } - QStandardItemModel* flat_model = new QStandardItemModel(this); - flat_model->setSortRole(GameListItemPath::SortRole); - for (int i = 0; i < hierarchical_model->rowCount(); ++i) { - QStandardItem* folder = hierarchical_model->item(i, 0); - if (!folder) continue; - const auto folder_type = folder->data(GameListItem::TypeRole).value(); - if (folder_type == GameListItemType::AddDir) { - continue; - } - for (int j = 0; j < folder->rowCount(); ++j) { - QStandardItem* game_item = folder->child(j, 0); - if (!game_item) continue; - const auto game_type = game_item->data(GameListItem::TypeRole).value(); - if (game_type == GameListItemType::Game) { - QStandardItem* cloned_item = game_item->clone(); - QString game_title = game_item->data(GameListItemPath::TitleRole).toString(); - if (game_title.isEmpty()) { - std::string filename; - Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); - game_title = QString::fromStdString(filename); - } - cloned_item->setText(game_title); - flat_model->appendRow(cloned_item); - } - } + if (sort_icon.isNull()) { + sort_icon = style()->standardIcon(QStyle::SP_ArrowUp); } - list_view->setModel(flat_model); - const u32 icon_size = UISettings::values.game_icon_size.GetValue(); - list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); - // Sort the grid view using current sort order - flat_model->sort(0, current_sort_order); - // Update icon sizes in the model - ensure all icons are consistently sized with rounded corners - for (int i = 0; i < flat_model->rowCount(); ++i) { - QStandardItem* item = flat_model->item(i); - if (item) { - QVariant icon_data = item->data(Qt::DecorationRole); - if (icon_data.isValid() && icon_data.canConvert()) { - QPixmap pixmap = icon_data.value(); - if (!pixmap.isNull()) { - #ifdef __linux__ - // On Linux, use simple scaling to avoid QPainter bugs - QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - item->setData(scaled, Qt::DecorationRole); - #else - // On other platforms, use the QPainter method for rounded corners - QPixmap rounded(icon_size, icon_size); - rounded.fill(Qt::transparent); - - QPainter painter(&rounded); - painter.setRenderHint(QPainter::Antialiasing); - - const int radius = icon_size / 8; - QPainterPath path; - path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius); - painter.setClipPath(path); - - QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - painter.drawPixmap(0, 0, scaled); - - item->setData(rounded, Qt::DecorationRole); - #endif - } - } - } + } else { + // Descending (Z-A) - arrow down + sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-descending")); + if (sort_icon.isNull()) { + sort_icon = QIcon::fromTheme(QStringLiteral("sort-descending")); + } + if (sort_icon.isNull()) { + sort_icon = style()->standardIcon(QStyle::SP_ArrowDown); } } - - void GameList::ToggleViewMode() { - bool current_grid_view = UISettings::values.game_list_grid_view.GetValue(); - UISettings::values.game_list_grid_view.SetValue(!current_grid_view); - SetViewMode(!current_grid_view); - // Button states are updated in SetViewMode - } - - void GameList::SortAlphabetically() { - if (tree_view->isVisible()) { - // Sort tree view by name column using current sort order - tree_view->header()->setSortIndicator(COLUMN_NAME, current_sort_order); - item_model->sort(COLUMN_NAME, current_sort_order); - } else if (list_view->isVisible()) { - // Sort grid view alphabetically using current sort order - QAbstractItemModel* current_model = list_view->model(); - if (current_model && current_model != item_model) { - // Sort the flat model used by list view (filtered or unfiltered) - QStandardItemModel* flat_model = qobject_cast(current_model); - if (flat_model) { - // Use SortRole for proper alphabetical sorting - flat_model->setSortRole(GameListItemPath::SortRole); - flat_model->sort(0, current_sort_order); - } - } else { - // If using item_model directly, repopulate grid view to apply sort - // Preserve filter if active - QString filter_text = search_field->filterText(); - if (!filter_text.isEmpty()) { - FilterGridView(filter_text); - } else { - PopulateGridView(); - } - } - } - UpdateSortButtonIcon(); - } - - void GameList::ToggleSortOrder() { - // Toggle between ascending and descending, just like clicking the Name column header - current_sort_order = (current_sort_order == Qt::AscendingOrder) - ? Qt::DescendingOrder - : Qt::AscendingOrder; - SortAlphabetically(); - } - - void GameList::UpdateSortButtonIcon() { - if (!btn_sort_az) return; - - QIcon sort_icon; - if (current_sort_order == Qt::AscendingOrder) { - // Ascending (A-Z) - arrow up - sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-ascending")); - if (sort_icon.isNull()) { - sort_icon = QIcon::fromTheme(QStringLiteral("sort-ascending")); - } - if (sort_icon.isNull()) { - sort_icon = style()->standardIcon(QStyle::SP_ArrowUp); - } - } else { - // Descending (Z-A) - arrow down - sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-descending")); - if (sort_icon.isNull()) { - sort_icon = QIcon::fromTheme(QStringLiteral("sort-descending")); - } - if (sort_icon.isNull()) { - sort_icon = style()->standardIcon(QStyle::SP_ArrowDown); - } - } - btn_sort_az->setIcon(sort_icon); - } + btn_sort_az->setIcon(sort_icon); +} void GameList::UpdateProgressBarColor() { - if (!progress_bar) return; + if (!progress_bar) + return; // Convert the Hex String from settings to a QColor QColor accent(QString::fromStdString(UISettings::values.accent_color.GetValue())); @@ -2165,18 +2394,18 @@ void GameList::UpdateProgressBarColor() { "background: qlineargradient(x1:0, y1:0, x2:1, y2:0, " "stop:0 #ff0000, stop:0.16 #ffff00, stop:0.33 #00ff00, " "stop:0.5 #00ffff, stop:0.66 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000); " - "}" - )); + "}")); } else { - progress_bar->setStyleSheet(QStringLiteral( - "QProgressBar { border: none; background: transparent; } " - "QProgressBar::chunk { background-color: %1; }" - ).arg(accent.name())); + progress_bar->setStyleSheet( + QStringLiteral("QProgressBar { border: none; background: transparent; } " + "QProgressBar::chunk { background-color: %1; }") + .arg(accent.name())); } } void GameList::RefreshCompatibilityList() { - const QUrl url(QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Compatability/refs/heads/main/compatibility_list.json")); + const QUrl url(QStringLiteral("https://raw.githubusercontent.com/CollectingW/" + "Citron-Compatability/refs/heads/main/compatibility_list.json")); QNetworkRequest request(url); QNetworkReply* reply = network_manager->get(request); @@ -2185,8 +2414,10 @@ void GameList::RefreshCompatibilityList() { if (reply->error() == QNetworkReply::NoError) { const QByteArray json_data = reply->readAll(); - const auto config_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir)); - const QString local_path = QDir(config_dir).filePath(QStringLiteral("compatibility_list.json")); + const auto config_dir = QString::fromStdString( + Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir)); + const QString local_path = + QDir(config_dir).filePath(QStringLiteral("compatibility_list.json")); QFile file(local_path); if (file.open(QFile::WriteOnly)) { @@ -2199,24 +2430,30 @@ void GameList::RefreshCompatibilityList() { // Refresh the UI by replacing the old compatibility items with new ones for (int i = 0; i < item_model->rowCount(); ++i) { QStandardItem* folder = item_model->item(i, 0); - if (!folder) continue; + if (!folder) + continue; for (int j = 0; j < folder->rowCount(); ++j) { QStandardItem* game_item = folder->child(j, 0); - if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) { + if (!game_item || + game_item->data(GameListItem::TypeRole).value() != + GameListItemType::Game) { continue; } - u64 program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong(); + u64 program_id = + game_item->data(GameListItemPath::ProgramIdRole).toULongLong(); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); if (it != compatibility_list.end()) { - folder->setChild(j, COLUMN_COMPATIBILITY, new GameListItemCompat(it->second.first)); + folder->setChild(j, COLUMN_COMPATIBILITY, + new GameListItemCompat(it->second.first)); } } } } } else { - LOG_ERROR(Frontend, "Failed to download compatibility list: {}", reply->errorString().toStdString()); + LOG_ERROR(Frontend, "Failed to download compatibility list: {}", + reply->errorString().toStdString()); } reply->deleteLater(); }); @@ -2228,38 +2465,48 @@ void GameList::onSurpriseMeClicked() { // Go through the list and gather info for every game (name, icon, path) for (int i = 0; i < item_model->rowCount(); ++i) { QStandardItem* folder = item_model->item(i, 0); - if (!folder || folder->data(GameListItem::TypeRole).value() == GameListItemType::AddDir) { + if (!folder || folder->data(GameListItem::TypeRole).value() == + GameListItemType::AddDir) { continue; } for (int j = 0; j < folder->rowCount(); ++j) { QStandardItem* game_item = folder->child(j, 0); - if (game_item && game_item->data(GameListItem::TypeRole).value() == GameListItemType::Game) { + if (game_item && game_item->data(GameListItem::TypeRole).value() == + GameListItemType::Game) { QString game_title = game_item->data(GameListItemPath::TitleRole).toString(); if (game_title.isEmpty()) { std::string filename; - Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); + Common::SplitPath( + game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), + nullptr, &filename, nullptr); game_title = QString::fromStdString(filename); } QPixmap icon = game_item->data(Qt::DecorationRole).value(); if (icon.isNull()) { // Use a generic icon if a game is missing one - icon = QIcon::fromTheme(QStringLiteral("application-x-executable")).pixmap(128, 128); + icon = QIcon::fromTheme(QStringLiteral("application-x-executable")) + .pixmap(128, 128); } - all_games.append({ - game_title, - game_item->data(GameListItemPath::FullPathRole).toString(), - static_cast(game_item->data(GameListItemPath::ProgramIdRole).toULongLong()), - icon - }); + if (UISettings::values.hidden_paths.contains( + game_item->data(GameListItemPath::FullPathRole).toString())) { + continue; + } + + all_games.append( + {game_title, game_item->data(GameListItemPath::FullPathRole).toString(), + static_cast( + game_item->data(GameListItemPath::ProgramIdRole).toULongLong()), + icon}); } } } if (all_games.empty()) { - QMessageBox::information(this, tr("Surprise Me!"), tr("No games available to choose from!")); + QMessageBox::information(this, tr("Surprise Me!"), + tr("No games available to choose from!")); return; } @@ -2289,100 +2536,101 @@ void GameList::UpdateAccentColorStyles() { QColor selection_background_color = accent_color; selection_background_color.setAlphaF(0.25f); // 25% opacity for a clear selection const QString selection_background_color_name = QStringLiteral("rgba(%1, %2, %3, %4)") - .arg(selection_background_color.red()) - .arg(selection_background_color.green()) - .arg(selection_background_color.blue()) - .arg(selection_background_color.alpha()); + .arg(selection_background_color.red()) + .arg(selection_background_color.green()) + .arg(selection_background_color.blue()) + .arg(selection_background_color.alpha()); // Create a MORE subtle semi-transparent version for the HOVER effect QColor hover_background_color = accent_color; hover_background_color.setAlphaF(0.15f); // 15% opacity for a subtle hover const QString hover_background_color_name = QStringLiteral("rgba(%1, %2, %3, %4)") - .arg(hover_background_color.red()) - .arg(hover_background_color.green()) - .arg(hover_background_color.blue()) - .arg(hover_background_color.alpha()); + .arg(hover_background_color.red()) + .arg(hover_background_color.green()) + .arg(hover_background_color.blue()) + .arg(hover_background_color.alpha()); - QString accent_style = QStringLiteral( - /* Tree View (List View) Selection & Hover Style */ - "QTreeView::item:hover {" - " background-color: %3;" /* Use the new accent-based hover color */ - " border-radius: 4px;" /* Add a subtle rounding to the hover effect */ - "}" - "QTreeView::item:selected {" - " background-color: %2;" /* Use the accent-based selection color */ - " color: palette(text);" - " border: none;" /* NO BORDER */ - " border-radius: 4px;" /* Keep rounding consistent */ - "}" - "QTreeView::item:selected:!active {" - " background-color: palette(light);" /* Use a muted color when window is not focused */ - " border: none;" /* NO BORDER */ - "}" + QString accent_style = + QStringLiteral( + /* Tree View (List View) Selection & Hover Style */ + "QTreeView::item:hover {" + " background-color: %3;" /* Use the new accent-based hover color */ + " border-radius: 4px;" /* Add a subtle rounding to the hover effect */ + "}" + "QTreeView::item:selected {" + " background-color: %2;" /* Use the accent-based selection color */ + " color: palette(text);" + " border: none;" /* NO BORDER */ + " border-radius: 4px;" /* Keep rounding consistent */ + "}" + "QTreeView::item:selected:!active {" + " background-color: palette(light);" /* Use a muted color when window is not focused + */ + " border: none;" /* NO BORDER */ + "}" - /* List View (Grid View) Selection Style */ - "QListView::item:selected {" - " background-color: palette(light);" - " border: 3px solid %1;" - " border-radius: 12px;" - "}" - "QListView::item:selected:!active {" - " background-color: transparent;" - " border: 3px solid palette(mid);" - "}" + /* List View (Grid View) Selection Style */ + "QListView::item:selected {" + " background-color: palette(light);" + " border: 3px solid %1;" + " border-radius: 12px;" + "}" + "QListView::item:selected:!active {" + " background-color: transparent;" + " border: 3px solid palette(mid);" + "}" - /* ScrollBar Styling */ - "QScrollBar:vertical {" - " border: 1px solid black;" - " background: palette(base);" - " width: 12px;" - " margin: 0px;" - "}" - "QScrollBar::handle:vertical {" - " background: %1;" - " min-height: 20px;" - " border-radius: 5px;" - " border: 1px solid black;" - "}" - ).arg(color_name, selection_background_color_name, hover_background_color_name); + /* ScrollBar Styling */ + "QScrollBar:vertical {" + " border: 1px solid black;" + " background: palette(base);" + " width: 12px;" + " margin: 0px;" + "}" + "QScrollBar::handle:vertical {" + " background: %1;" + " min-height: 20px;" + " border-radius: 5px;" + " border: 1px solid black;" + "}") + .arg(color_name, selection_background_color_name, hover_background_color_name); // Apply the combined base styles and new accent styles to each view tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }") + accent_style); - list_view->setStyleSheet(QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { text-align: center; padding: 5px; }") + accent_style); + list_view->setStyleSheet( + QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { " + "text-align: center; padding: 5px; }") + + accent_style); // Update the toolbar buttons as before - QString button_base_style = QStringLiteral( - "QToolButton {" - " border: 1px solid palette(mid);" - " border-radius: 4px;" - " background: palette(button);" - "}" - "QToolButton:hover {" - " background: palette(light);" - "}" - ); - QString button_checked_style = QStringLiteral( - "QToolButton:checked {" - " background: %1;" - " border-color: %1;" - "}" - ).arg(color_name); + QString button_base_style = QStringLiteral("QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}"); + QString button_checked_style = QStringLiteral("QToolButton:checked {" + " background: %1;" + " border-color: %1;" + "}") + .arg(color_name); btn_list_view->setStyleSheet(button_base_style + button_checked_style); btn_grid_view->setStyleSheet(button_base_style + button_checked_style); - search_field->setStyleSheet(QStringLiteral( - "QLineEdit {" - " border: 1px solid palette(mid);" - " border-radius: 6px;" - " padding: 4px 8px;" - " background: palette(base);" - "}" - "QLineEdit:focus {" - " border: 1px solid %1;" - " background: palette(base);" - "}" - ).arg(color_name)); + search_field->setStyleSheet(QStringLiteral("QLineEdit {" + " border: 1px solid palette(mid);" + " border-radius: 6px;" + " padding: 4px 8px;" + " background: palette(base);" + "}" + "QLineEdit:focus {" + " border: 1px solid %1;" + " background: palette(base);" + "}") + .arg(color_name)); } void GameList::ToggleHidden(const QString& path) { @@ -2419,9 +2667,7 @@ void GameList::OnEmulationEnded() { fade_out_anim->setEasingCurve(QEasingCurve::OutQuad); // When the fade-out is complete, hide the overlay widget - connect(fade_out_anim, &QPropertyAnimation::finished, this, [this]() { - fade_overlay->hide(); - }); + connect(fade_out_anim, &QPropertyAnimation::finished, this, [this]() { fade_overlay->hide(); }); fade_out_anim->start(QAbstractAnimation::DeleteWhenStopped); } diff --git a/src/citron/game_list.h b/src/citron/game_list.h index dc591957e..ec919bf25 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -12,27 +12,29 @@ #include #include #include -#include -#include +#include +#include #include +#include +#include +#include #include #include -#include #include #include #include #include #include #include -#include -#include + +#include "citron/compatibility_list.h" +#include "citron/multiplayer/state.h" +#include "citron/play_time_manager.h" #include "common/common_types.h" #include "core/core.h" #include "uisettings.h" -#include "citron/compatibility_list.h" -#include "citron/play_time_manager.h" -#include "citron/multiplayer/state.h" + class ControllerNavigation; class GameListWorker; @@ -43,8 +45,8 @@ enum class AmLaunchType; enum class StartGameType; namespace FileSys { - class ManualContentProvider; - class VfsFilesystem; +class ManualContentProvider; +class VfsFilesystem; } // namespace FileSys enum class GameListOpenTarget { @@ -190,8 +192,10 @@ private: void FilterTreeView(const QString& filter_text); void PopupContextMenu(const QPoint& menu_location); - void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path, const QString& game_name); - void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); + void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path, + const QString& game_name); + void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected, + bool show_hidden_action = true); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); void AddFavoritesPopup(QMenu& context_menu); diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index b97225258..0a57cbaa8 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -11,16 +11,22 @@ #include #include +#include #include #include #include -#include +#include #include #include -#include +#include #include -#include + +#include "citron/compatibility_list.h" +#include "citron/game_list.h" +#include "citron/game_list_p.h" +#include "citron/game_list_worker.h" +#include "citron/uisettings.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" #include "core/core.h" @@ -33,11 +39,7 @@ #include "core/file_sys/registered_cache.h" #include "core/file_sys/submission_package.h" #include "core/loader/loader.h" -#include "citron/compatibility_list.h" -#include "citron/game_list.h" -#include "citron/game_list_p.h" -#include "citron/game_list_worker.h" -#include "citron/uisettings.h" + namespace { @@ -76,9 +78,8 @@ std::string GetCacheKey(const std::string& file_path) { } const auto path_str = Common::FS::PathToUTF8String(normalized_path); - const auto hash = QCryptographicHash::hash( - QByteArray::fromStdString(path_str), - QCryptographicHash::Sha256); + const auto hash = + QCryptographicHash::hash(QByteArray::fromStdString(path_str), QCryptographicHash::Sha256); return hash.toHex().toStdString(); } @@ -90,7 +91,8 @@ void LoadGameMetadataCache() { game_metadata_cache.clear(); - const auto cache_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list"; + const auto cache_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list"; const auto cache_file = Common::FS::PathToUTF8String(cache_dir / "game_metadata_cache.json"); if (!Common::FS::Exists(cache_file)) { @@ -115,15 +117,19 @@ void LoadGameMetadataCache() { const std::string key = entry[QStringLiteral("key")].toString().toStdString(); CachedGameMetadata metadata; - metadata.program_id = entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16); - metadata.file_type = static_cast(entry[QStringLiteral("file_type")].toInt()); - metadata.file_size = static_cast(entry[QStringLiteral("file_size")].toVariant().toULongLong()); + metadata.program_id = + entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16); + metadata.file_type = + static_cast(entry[QStringLiteral("file_type")].toInt()); + metadata.file_size = + static_cast(entry[QStringLiteral("file_size")].toVariant().toULongLong()); metadata.title = entry[QStringLiteral("title")].toString().toStdString(); metadata.file_path = entry[QStringLiteral("file_path")].toString().toStdString(); - metadata.modification_time = entry[QStringLiteral("modification_time")].toVariant().toLongLong(); + metadata.modification_time = + entry[QStringLiteral("modification_time")].toVariant().toLongLong(); - const QByteArray icon_data = QByteArray::fromBase64( - entry[QStringLiteral("icon")].toString().toUtf8()); + const QByteArray icon_data = + QByteArray::fromBase64(entry[QStringLiteral("icon")].toString().toUtf8()); metadata.icon.assign(icon_data.begin(), icon_data.end()); if (metadata.IsValid()) { @@ -138,7 +144,8 @@ void SaveGameMetadataCache() { return; } - const auto cache_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list"; + const auto cache_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list"; const auto cache_file = Common::FS::PathToUTF8String(cache_dir / "game_metadata_cache.json"); void(Common::FS::CreateParentDirs(cache_file)); @@ -154,7 +161,8 @@ void SaveGameMetadataCache() { entry[QStringLiteral("file_size")] = static_cast(metadata.file_size); entry[QStringLiteral("title")] = QString::fromStdString(metadata.title); entry[QStringLiteral("file_path")] = QString::fromStdString(metadata.file_path); - entry[QStringLiteral("modification_time")] = static_cast(metadata.modification_time); + entry[QStringLiteral("modification_time")] = + static_cast(metadata.modification_time); const QByteArray icon_data(reinterpret_cast(metadata.icon.data()), static_cast(metadata.icon.size())); @@ -189,8 +197,8 @@ const CachedGameMetadata* GetCachedGameMetadata(const std::string& file_path) { return nullptr; } - const auto mod_time_seconds = std::chrono::duration_cast( - mod_time.time_since_epoch()).count(); + const auto mod_time_seconds = + std::chrono::duration_cast(mod_time.time_since_epoch()).count(); const std::string key = GetCacheKey(file_path); const auto it = game_metadata_cache.find(key); @@ -211,7 +219,8 @@ const CachedGameMetadata* GetCachedGameMetadata(const std::string& file_path) { // Store game metadata in cache void CacheGameMetadata(const std::string& file_path, u64 program_id, Loader::FileType file_type, - std::size_t file_size, const std::string& title, const std::vector& icon) { + std::size_t file_size, const std::string& title, + const std::vector& icon) { if (!UISettings::values.cache_game_list) { return; } @@ -222,8 +231,8 @@ void CacheGameMetadata(const std::string& file_path, u64 program_id, Loader::Fil return; } - const auto mod_time_seconds = std::chrono::duration_cast( - mod_time.time_since_epoch()).count(); + const auto mod_time_seconds = + std::chrono::duration_cast(mod_time.time_since_epoch()).count(); const std::string key = GetCacheKey(file_path); @@ -411,12 +420,13 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, return out; } -QList MakeGameListEntry( - const std::string& path, const std::string& name, const std::size_t size, - const std::vector& icon, Loader::AppLoader& loader, u64 program_id, - const CompatibilityList& compatibility_list, const PlayTime::PlayTimeManager& play_time_manager, - const FileSys::PatchManager& patch, - const std::map>& online_stats) { +QList MakeGameListEntry(const std::string& path, const std::string& name, + const std::size_t size, const std::vector& icon, + Loader::AppLoader& loader, u64 program_id, + const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager, + const FileSys::PatchManager& patch, + const std::map>& online_stats) { const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); // The game list uses this as compatibility number for untested games @@ -432,17 +442,18 @@ QList MakeGameListEntry( auto it_stats = online_stats.find(program_id); if (it_stats != online_stats.end()) { const auto& stats = it_stats->second; - online_text = QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second); + online_text = + QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second); } - QList list{ - new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name), - file_type_string, program_id), - new GameListItemCompat(compatibility), - new GameListItem(file_type_string), - new GameListItemSize(size), - new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), - new GameListItemOnline(online_text)}; + QList list{new GameListItemPath(FormatGameName(path), icon, + QString::fromStdString(name), file_type_string, + program_id), + new GameListItemCompat(compatibility), + new GameListItem(file_type_string), + new GameListItemSize(size), + new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), + new GameListItemOnline(online_text)}; const auto patch_versions = GetGameListCachedObject( fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] { @@ -510,7 +521,8 @@ void GameListWorker::RecordEvent(F&& func) { emit DataAvailable(); } -void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, const std::map>& online_stats) { +void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, + const std::map>& online_stats) { using namespace FileSys; const auto& cache = system.GetContentProviderUnion(); @@ -556,17 +568,21 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, const std::map GetMetadataFromControlNCA(patch, *control, icon, name); } - auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, - program_id, compatibility_list, play_time_manager, patch, online_stats); + auto entry = + MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, program_id, + compatibility_list, play_time_manager, patch, online_stats); RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); } } void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, - GameListDir* parent_dir, const std::map>& online_stats, + GameListDir* parent_dir, + const std::map>& online_stats, int& processed_files, int total_files) { - const auto callback = [this, target, parent_dir, &online_stats, &processed_files, total_files](const std::filesystem::path& path) -> bool { - if (stop_requested) return false; + const auto callback = [this, target, parent_dir, &online_stats, &processed_files, + total_files](const std::filesystem::path& path) -> bool { + if (stop_requested) + return false; const auto physical_name = Common::FS::PathToUTF8String(path); @@ -583,19 +599,49 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa // Cache Check const auto* cached = GetCachedGameMetadata(physical_name); - if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { - if ((cached->program_id & 0xFFF) == 0) { - const FileSys::PatchManager patch{cached->program_id, system.GetFileSystemController(), system.GetContentProvider()}; - auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); - if (file) { + if (cached && cached->IsValid() && + (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { + auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); + if (file) { + // 1. Register with provider if requested + if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { + if (cached->file_type == Loader::FileType::NCA) { + provider->AddEntry( + FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + cached->program_id, file); + } else if (cached->file_type == Loader::FileType::XCI || + cached->file_type == Loader::FileType::NSP) { + const auto nsp = cached->file_type == Loader::FileType::NSP + ? std::make_shared(file) + : FileSys::XCI{file}.GetSecurePartitionNSP(); + for (const auto& title : nsp->GetNCAs()) { + for (const auto& entry : title.second) { + provider->AddEntry(entry.first.first, entry.first.second, + title.first, entry.second->GetBaseFile()); + } + } + } + } + + // 2. Populate UI if requested (only for base games) + if ((cached->program_id & 0xFFF) == 0 && + (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { + const FileSys::PatchManager patch{cached->program_id, + system.GetFileSystemController(), + system.GetContentProvider()}; auto loader = Loader::GetLoader(system, file); if (loader) { - auto entry = MakeGameListEntry(physical_name, cached->title, cached->file_size, cached->icon, *loader, - cached->program_id, compatibility_list, play_time_manager, patch, online_stats); - RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); + auto entry = MakeGameListEntry(physical_name, cached->title, + cached->file_size, cached->icon, *loader, + cached->program_id, compatibility_list, + play_time_manager, patch, online_stats); + RecordEvent( + [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); } } } + processed_files++; emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files)); return true; @@ -622,12 +668,18 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { if (file_type == Loader::FileType::NCA) { - provider->AddEntry(FileSys::TitleType::Application, FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, file); - } else if (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) { - const auto nsp = file_type == Loader::FileType::NSP ? std::make_shared(file) : FileSys::XCI{file}.GetSecurePartitionNSP(); + provider->AddEntry(FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + program_id, file); + } else if (file_type == Loader::FileType::XCI || + file_type == Loader::FileType::NSP) { + const auto nsp = file_type == Loader::FileType::NSP + ? std::make_shared(file) + : FileSys::XCI{file}.GetSecurePartitionNSP(); for (const auto& title : nsp->GetNCAs()) { for (const auto& entry : title.second) { - provider->AddEntry(entry.first.first, entry.first.second, title.first, entry.second->GetBaseFile()); + provider->AddEntry(entry.first.first, entry.first.second, title.first, + entry.second->GetBaseFile()); } } } @@ -644,9 +696,13 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon); - const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()}; - auto entry = MakeGameListEntry(physical_name, name, file_size, icon, *loader, program_id, compatibility_list, play_time_manager, patch, online_stats); - RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); + const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), + system.GetContentProvider()}; + auto entry = MakeGameListEntry(physical_name, name, file_size, icon, *loader, + program_id, compatibility_list, + play_time_manager, patch, online_stats); + RecordEvent( + [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); } } } @@ -657,7 +713,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa }; if (deep_scan) { - Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::File); + Common::FS::IterateDirEntriesRecursively(dir_path, callback, + Common::FS::DirEntryFilter::File); } else { Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); } @@ -685,7 +742,8 @@ void GameListWorker::run() { int processed_files = 0; for (const auto& game_dir : game_dirs) { - if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") + continue; auto count_callback = [&](const std::filesystem::path& path) -> bool { const std::string physical_name = Common::FS::PathToUTF8String(path); @@ -696,28 +754,34 @@ void GameListWorker::run() { }; if (game_dir.deep_scan) { - Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, Common::FS::DirEntryFilter::File); + Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, + Common::FS::DirEntryFilter::File); } else { - Common::FS::IterateDirEntries(game_dir.path, count_callback, Common::FS::DirEntryFilter::File); + Common::FS::IterateDirEntries(game_dir.path, count_callback, + Common::FS::DirEntryFilter::File); } } - if (total_files <= 0) total_files = 1; + if (total_files <= 0) + total_files = 1; const auto DirEntryReady = [&](GameListDir* game_list_dir) { RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); }); }; for (UISettings::GameDir& game_dir : game_dirs) { - if (stop_requested) break; + if (stop_requested) + break; - if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") + continue; watch_list.append(QString::fromStdString(game_dir.path)); auto* const game_list_dir = new GameListDir(game_dir); DirEntryReady(game_list_dir); - ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files); + ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, + online_stats, processed_files, total_files); } RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); }); diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 27a038de6..b8d348c70 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -37,16 +37,19 @@ #include "applets/qt_profile_select.h" #include "applets/qt_software_keyboard.h" #include "applets/qt_web_browser.h" +#include "citron/multiplayer/state.h" +#include "citron/setup_wizard.h" +#include "citron/util/controller_navigation.h" #include "common/hex_util.h" #include "common/nvidia_flags.h" #include "common/settings_enums.h" #include "configuration/configure_input.h" #include "configuration/configure_per_game.h" #include "configuration/configure_tas.h" +#include "core/file_sys/nca_metadata.h" #include "core/file_sys/romfs_factory.h" #include "core/file_sys/vfs/vfs.h" #include "core/file_sys/vfs/vfs_real.h" -#include "core/file_sys/nca_metadata.h" #include "core/frontend/applets/cabinet.h" #include "core/frontend/applets/controller.h" #include "core/frontend/applets/general.h" @@ -58,9 +61,6 @@ #include "frontend_common/content_manager.h" #include "hid_core/frontend/emulated_controller.h" #include "hid_core/hid_core.h" -#include "citron/multiplayer/state.h" -#include "citron/setup_wizard.h" -#include "citron/util/controller_navigation.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows // defines. @@ -78,8 +78,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include #define QT_NO_OPENGL -#include #include +#include #include #include #include @@ -123,6 +123,30 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #ifdef ARCHITECTURE_x86_64 #include "common/x64/cpu_detect.h" #endif +#include "citron/about_dialog.h" +#include "citron/bootmanager.h" +#include "citron/compatdb.h" +#include "citron/compatibility_list.h" +#include "citron/configuration/configure_dialog.h" +#include "citron/configuration/configure_filesystem.h" +#include "citron/configuration/configure_input_per_game.h" +#include "citron/configuration/qt_config.h" +#include "citron/controller_overlay.h" +#include "citron/debugger/console.h" +#include "citron/debugger/controller.h" +#include "citron/debugger/profiler.h" +#include "citron/debugger/wait_tree.h" +#include "citron/discord.h" +#include "citron/game_list.h" +#include "citron/game_list_p.h" +#include "citron/hotkeys.h" +#include "citron/install_dialog.h" +#include "citron/loading_screen.h" +#include "citron/main.h" +#include "citron/play_time_manager.h" +#include "citron/startup_checks.h" +#include "citron/uisettings.h" +#include "citron/util/rainbow_style.h" #include "common/settings.h" #include "common/string_util.h" #include "common/telemetry.h" @@ -157,38 +181,14 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/shader_notify.h" -#include "citron/about_dialog.h" -#include "citron/bootmanager.h" -#include "citron/compatdb.h" -#include "citron/controller_overlay.h" -#include "core/core.h" -#include "citron/compatibility_list.h" -#include "citron/configuration/configure_dialog.h" -#include "citron/configuration/configure_filesystem.h" -#include "citron/configuration/configure_input_per_game.h" -#include "citron/configuration/qt_config.h" -#include "citron/debugger/console.h" -#include "citron/debugger/controller.h" -#include "citron/debugger/profiler.h" -#include "citron/debugger/wait_tree.h" -#include "citron/discord.h" -#include "citron/game_list.h" -#include "citron/game_list_p.h" -#include "citron/hotkeys.h" -#include "citron/install_dialog.h" -#include "citron/loading_screen.h" -#include "citron/main.h" -#include "citron/play_time_manager.h" -#include "citron/startup_checks.h" -#include "citron/util/rainbow_style.h" -#include "citron/uisettings.h" + #ifdef CITRON_USE_AUTO_UPDATER #include "citron/updater/updater_dialog.h" #include "citron/updater/updater_service.h" #endif #include "citron/util/clickable_label.h" -#include "citron/util/performance_overlay.h" #include "citron/util/multiplayer_room_overlay.h" +#include "citron/util/performance_overlay.h" #include "citron/util/vram_overlay.h" #include "citron/vk_device_info.h" @@ -207,8 +207,9 @@ Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #endif #ifdef _WIN32 -#include #include +#include + extern "C" { // tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable // graphics @@ -345,8 +346,7 @@ bool GMainWindow::CheckDarkMode() { GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulkan) : system{std::make_unique()}, input_subsystem{std::make_shared()}, - ui{std::make_unique()}, - config{std::move(config_)}, + ui{std::make_unique()}, config{std::move(config_)}, vfs{std::make_shared()}, provider{std::make_unique()} { #ifdef __unix__ @@ -404,7 +404,8 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk const auto description = std::string(Common::g_scm_desc); const auto build_id = std::string(Common::g_build_id); - const auto citron_build = fmt::format("citron Development Build | {}-{}", branch_name, description); + const auto citron_build = + fmt::format("citron Development Build | {}-{}", branch_name, description); const auto override_build = fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id); const auto citron_build_version = override_build.empty() ? citron_build : override_build; @@ -445,19 +446,18 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk #endif UpdateWindowTitle(); + LOG_INFO(Frontend, "Registering system content providers..."); system->SetContentProvider(std::make_unique()); - system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); - system->GetFileSystemController().CreateFactories(*vfs); - - system->SetContentProvider(std::make_unique()); - system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); + system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, + provider.get()); // 1. First, create the factories + LOG_INFO(Frontend, "Initializing factories..."); system->GetFileSystemController().CreateFactories(*vfs); autoloader_provider = std::make_unique(); system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::Autoloader, - autoloader_provider.get()); + autoloader_provider.get()); // Remove cached contents generated during the previous session RemoveCachedContents(); @@ -496,16 +496,19 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk QTimer::singleShot(0, this, [this]() { LOG_INFO(Frontend, "Executing deferred first-time setup check."); - // Create a non-modal QMessageBox instance with a nullptr parent to make it a top-level window. - // This prevents it from blocking the main application window. + // Create a non-modal QMessageBox instance with a nullptr parent to make it a top-level + // window. This prevents it from blocking the main application window. auto* confirmation_dialog = new QMessageBox(nullptr); - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || + qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (is_gamescope) { - confirmation_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint); + confirmation_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | + Qt::WindowTitleHint | Qt::WindowStaysOnTopHint); confirmation_dialog->resize(650, 300); confirmation_dialog->setStyleSheet(QStringLiteral("font-size: 11pt;")); } - confirmation_dialog->setAttribute(Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close. + confirmation_dialog->setAttribute( + Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close. confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality. confirmation_dialog->setWindowTitle(tr("First-Time Setup")); confirmation_dialog->setText(tr("Would you like to run the first-time setup wizard?")); @@ -545,8 +548,10 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk if (has_broken_vulkan) { UISettings::values.has_broken_vulkan = true; - QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"), - tr("Vulkan initialization failed during boot.

For support, please visit Help > Get Support (Discord) in the main emulation window.")); + QMessageBox::warning( + this, tr("Broken Vulkan Installation Detected"), + tr("Vulkan initialization failed during boot.

For support, please visit Help " + "> Get Support (Discord) in the main emulation window.")); #ifdef HAS_OPENGL Settings::values.renderer_backend = Settings::RendererBackend::OpenGL; @@ -662,13 +667,13 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk } if (!game_path.isEmpty()) { - // Defer the game boot until after the main event loop has started. - QTimer::singleShot(0, this, [this, game_path, is_fullscreen, has_gamepath]() { - if (has_gamepath || is_fullscreen) { - ui->action_Fullscreen->setChecked(is_fullscreen); - } - BootGame(game_path, ApplicationAppletParameters()); - }); + // Defer the game boot until after the main event loop has started. + QTimer::singleShot(0, this, [this, game_path, is_fullscreen, has_gamepath]() { + if (has_gamepath || is_fullscreen) { + ui->action_Fullscreen->setChecked(is_fullscreen); + } + BootGame(game_path, ApplicationAppletParameters()); + }); } } @@ -875,8 +880,7 @@ void GMainWindow::SoftwareKeyboardShowNormal() { const auto w = layout.screen.GetWidth(); const auto h = layout.screen.GetHeight(); - software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)), - QSize(w, h)); + software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)), QSize(w, h)); } void GMainWindow::SoftwareKeyboardShowTextCheck( @@ -911,8 +915,7 @@ void GMainWindow::SoftwareKeyboardShowInline( const auto h = static_cast(layout.screen.GetHeight() * appear_parameters.key_top_scale_y); software_keyboard->ShowInlineKeyboard(std::move(appear_parameters), - render_window->mapToGlobal(QPoint(x, y)), - QSize(w, h)); + render_window->mapToGlobal(QPoint(x, y)), QSize(w, h)); } void GMainWindow::SoftwareKeyboardHideInline() { @@ -993,10 +996,9 @@ void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url, const auto& layout = render_window->GetFramebufferLayout(); web_applet->resize(layout.screen.GetWidth(), layout.screen.GetHeight()); - web_applet->move(layout.screen.left, - (layout.screen.top) + menuBar()->height()); + web_applet->move(layout.screen.left, (layout.screen.top) + menuBar()->height()); web_applet->setZoomFactor(static_cast(layout.screen.GetWidth()) / - static_cast(Layout::ScreenUndocked::Width)); + static_cast(Layout::ScreenUndocked::Width)); web_applet->setFocus(); web_applet->show(); @@ -1097,7 +1099,8 @@ void GMainWindow::InitializeWidgets() { #ifdef CITRON_ENABLE_COMPATIBILITY_REPORTING ui->action_Report_Compatibility->setVisible(true); #endif - render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system, hotkey_registry); + render_window = + new GRenderWindow(this, emu_thread.get(), input_subsystem, *system, hotkey_registry); render_window->hide(); game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this); @@ -1362,15 +1365,17 @@ void GMainWindow::InitializeWidgets() { statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (is_gamescope) { statusBar()->setSizeGripEnabled(true); this->menuBar()->setNativeMenuBar(false); QString gamescope_style = qApp->styleSheet(); - gamescope_style.append(QStringLiteral("QMenu { background-color: #2b2b2b; border: 1px solid #3d3d3d; padding: 2px; } " - "QMenu::item { padding: 5px 25px 5px 20px; } " - "QMenu::item:selected { background-color: #3d3d3d; }")); + gamescope_style.append(QStringLiteral( + "QMenu { background-color: #2b2b2b; border: 1px solid #3d3d3d; padding: 2px; } " + "QMenu::item { padding: 5px 25px 5px 20px; } " + "QMenu::item:selected { background-color: #3d3d3d; }")); qApp->setStyleSheet(gamescope_style); multiplayer_room_overlay->resize(360, 240); @@ -1465,18 +1470,22 @@ void GMainWindow::InitializeHotkeys() { LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); LinkActionShortcut(ui->action_Toggle_Grid_View, QStringLiteral("Toggle Grid View")); LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); - LinkActionShortcut(ui->action_Show_Performance_Overlay, QStringLiteral("Toggle Performance Overlay")); + LinkActionShortcut(ui->action_Show_Performance_Overlay, + QStringLiteral("Toggle Performance Overlay")); LinkActionShortcut(ui->action_Show_Vram_Overlay, QStringLiteral("Toggle VRAM Overlay")); LinkActionShortcut(ui->actionControllerOverlay, QStringLiteral("Toggle Controller Overlay")); - LinkActionShortcut(ui->action_Show_Multiplayer_Room_Overlay, QStringLiteral("Toggle Multiplayer Room Overlay")); + LinkActionShortcut(ui->action_Show_Multiplayer_Room_Overlay, + QStringLiteral("Toggle Multiplayer Room Overlay")); LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen")); LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); LinkActionShortcut(ui->action_TAS_Record, QStringLiteral("TAS Record"), true); LinkActionShortcut(ui->action_TAS_Reset, QStringLiteral("TAS Reset"), true); - LinkActionShortcut(ui->action_View_Lobby, QStringLiteral("Multiplayer Browse Public Game Lobby")); + LinkActionShortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); LinkActionShortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); - LinkActionShortcut(ui->action_Connect_To_Room, QStringLiteral("Multiplayer Direct Connect to Room")); + LinkActionShortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); LinkActionShortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); LinkActionShortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); @@ -1489,15 +1498,18 @@ void GMainWindow::InitializeHotkeys() { const auto connect_shortcut = [&](const QString& action_name, const Fn& function) { static const std::string main_window = "Main Window"; - const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name.toStdString(), this); + const auto* hotkey = + hotkey_registry.GetHotkey(main_window, action_name.toStdString(), this); auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); const auto* controller_hotkey = hotkey_registry.GetControllerHotkey(main_window, action_name.toStdString(), controller); connect(hotkey, &QShortcut::activated, this, function); - connect(controller_hotkey, &ControllerShortcut::Activated, this, function, Qt::QueuedConnection); + connect(controller_hotkey, &ControllerShortcut::Activated, this, function, + Qt::QueuedConnection); }; - connect_shortcut(QStringLiteral("Change Adapting Filter"), &GMainWindow::OnToggleAdaptingFilter); + connect_shortcut(QStringLiteral("Change Adapting Filter"), + &GMainWindow::OnToggleAdaptingFilter); connect_shortcut(QStringLiteral("Change Docked Mode"), &GMainWindow::OnToggleDockedMode); connect_shortcut(QStringLiteral("Change GPU Accuracy"), &GMainWindow::OnToggleGpuAccuracy); connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); @@ -1511,7 +1523,8 @@ void GMainWindow::InitializeHotkeys() { } void GMainWindow::SetDefaultUIGeometry() { - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (is_gamescope) { this->resize(1280, 800); @@ -1529,7 +1542,8 @@ void GMainWindow::SetDefaultUIGeometry() { } void GMainWindow::RestoreUIState() { - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); @@ -1548,10 +1562,10 @@ void GMainWindow::RestoreUIState() { render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } - #if MICROPROFILE_ENABLED +#if MICROPROFILE_ENABLED microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); - #endif +#endif game_list->LoadInterfaceLayout(); @@ -1639,7 +1653,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); - connect(game_list, &GameList::RunAutoloaderRequested, this, &GMainWindow::OnRunAutoloaderFromGameList); + connect(game_list, &GameList::RunAutoloaderRequested, this, + &GMainWindow::OnRunAutoloaderFromGameList); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); connect(game_list, &GameList::PopulatingCompleted, [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); @@ -1685,7 +1700,8 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Load_Folder, &GMainWindow::OnMenuLoadFolder); connect_menu(ui->action_Install_File_NAND, &GMainWindow::OnMenuInstallToNAND); - connect(ui->action_Install_With_Update_Manager, &QAction::triggered, this, &GMainWindow::OnMenuInstallWithUpdateManager); + connect(ui->action_Install_With_Update_Manager, &QAction::triggered, this, + &GMainWindow::OnMenuInstallWithUpdateManager); connect_menu(ui->action_Trim_XCI_File, &GMainWindow::OnMenuTrimXCI); connect_menu(ui->action_Exit, &QMainWindow::close); connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); @@ -1706,7 +1722,8 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar); connect_menu(ui->action_Show_Performance_Overlay, &GMainWindow::OnTogglePerformanceOverlay); - connect_menu(ui->action_Show_Multiplayer_Room_Overlay, &GMainWindow::OnToggleMultiplayerRoomOverlay); + connect_menu(ui->action_Show_Multiplayer_Room_Overlay, + &GMainWindow::OnToggleMultiplayerRoomOverlay); connect_menu(ui->action_Show_Vram_Overlay, &GMainWindow::OnToggleVramOverlay); connect_menu(ui->action_Toggle_Grid_View, &GMainWindow::OnToggleGridView); @@ -1753,14 +1770,15 @@ void GMainWindow::ConnectMenuEvents() { // Help connect_menu(ui->action_Open_citron_Folder, &GMainWindow::OnOpenCitronFolder); + connect_menu(ui->action_Open_Log_Folder, &GMainWindow::OnOpenLogFolder); connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware); connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); connect_menu(ui->action_About, &GMainWindow::OnAbout); - connect(ui->actionControllerOverlay, &QAction::triggered, this, &GMainWindow::OnToggleControllerOverlay); - + connect(ui->actionControllerOverlay, &QAction::triggered, this, + &GMainWindow::OnToggleControllerOverlay); } void GMainWindow::UpdateMenuState() { @@ -1973,7 +1991,8 @@ bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPa tr("You are using the deconstructed ROM directory format for this game, which is an " "outdated format that has been superseded by others such as NCA, NAX, XCI, or " "NSP. Deconstructed ROM directories lack icons, metadata, and update " - "support.

For support, please visit Help > Get Support (Discord) in the main emulation window. This message will not be shown again.")); + "support.

For support, please visit Help > Get Support (Discord) in " + "the main emulation window. This message will not be shown again.")); } if (result != Core::SystemResultStatus::Success) { @@ -2003,11 +2022,11 @@ bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPa const auto title = tr("Error while loading ROM! %1", "%1 signifies a numeric error code.") .arg(QString::fromStdString(error_code)); - const auto description = - tr("%1
For support, please visit Help > Get Support (Discord) in the main emulation window.", - "%1 signifies an error string.") - .arg(QString::fromStdString( - GetResultStatusString(static_cast(error_id)))); + const auto description = tr("%1
For support, please visit Help > Get Support " + "(Discord) in the main emulation window.", + "%1 signifies an error string.") + .arg(QString::fromStdString(GetResultStatusString( + static_cast(error_id)))); QMessageBox::critical(this, title, description); } else { @@ -2128,10 +2147,10 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP // Final Fantasy Tactics requires single-core mode to boot properly if (title_id == 0x010038B015560000ULL) { - LOG_INFO(Frontend, "Applying workaround: forcing single-core mode for Final Fantasy Tactics"); + LOG_INFO(Frontend, + "Applying workaround: forcing single-core mode for Final Fantasy Tactics"); Settings::values.use_multi_core.SetValue(false); } - } Settings::LogSettings(); @@ -2147,7 +2166,6 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP if (SelectAndSetCurrentUser(parameters) == false) { return; // User cancelled profile selection } - } user_flag_cmd_line = false; @@ -2321,7 +2339,8 @@ void GMainWindow::OnEmulationStopped() { // Reset the startup sync flag for the next session. has_performed_initial_sync = false; - LOG_INFO(Frontend, "Mirroring: Emulation stopped. Re-arming startup sync for next game list refresh."); + LOG_INFO(Frontend, + "Mirroring: Emulation stopped. Re-arming startup sync for next game list refresh."); // This is necessary to reset the in-memory state for the next launch. system->GetFileSystemController().CreateFactories(*vfs, true); @@ -2449,7 +2468,8 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target const std::string& mirrored_path_str = Settings::values.mirrored_save_paths.at(program_id); if (!mirrored_path_str.empty() && Common::FS::IsDir(mirrored_path_str)) { - LOG_INFO(Frontend, "Opening external mirrored save data path for program_id={:016x}", + LOG_INFO(Frontend, + "Opening external mirrored save data path for program_id={:016x}", program_id); QDesktopServices::openUrl( QUrl::fromLocalFile(QString::fromStdString(mirrored_path_str))); @@ -2460,24 +2480,30 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target else if (Settings::values.custom_save_paths.count(program_id)) { const std::string& custom_path_str = Settings::values.custom_save_paths.at(program_id); if (!custom_path_str.empty() && Common::FS::IsDir(custom_path_str)) { - LOG_INFO(Frontend, "Opening per-game custom save data path for program_id={:016x}", program_id); - QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(custom_path_str))); + LOG_INFO(Frontend, "Opening per-game custom save data path for program_id={:016x}", + program_id); + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(custom_path_str))); return; } } // 3. Priority 3: Global Custom Path - else if (Settings::values.global_custom_save_path_enabled.GetValue()) { + std::filesystem::path nand_dir; + if (Settings::values.global_custom_save_path_enabled.GetValue()) { const std::string& global_path_str = Settings::values.global_custom_save_path.GetValue(); if (!global_path_str.empty() && Common::FS::IsDir(global_path_str)) { - LOG_INFO(Frontend, "Opening global custom save data path for program_id={:016x}", - program_id); - QDesktopServices::openUrl( - QUrl::fromLocalFile(QString::fromStdString(global_path_str))); - return; + nand_dir = std::filesystem::path(global_path_str); } } + if (nand_dir.empty()) { + nand_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir); + } + + auto vfs_nand_dir = + vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read); + const auto [user_save_size, device_save_size] = [this, &game_path, &program_id] { const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), system->GetContentProvider()}; @@ -2491,7 +2517,8 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target FileSys::NACP nacp{}; loader->ReadControlData(nacp); - return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize()); + return std::make_pair(nacp.GetDefaultNormalSaveSize(), + nacp.GetDeviceSaveDataSize()); } }(); @@ -2500,10 +2527,6 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?"); - const auto nand_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir); - auto vfs_nand_dir = - vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read); - if (has_user_save) { // User save data const auto select_profile = [this] { @@ -2935,10 +2958,10 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa } const auto base_romfs = base_nca->GetRomFS(); - const auto dump_dir = - target == DumpRomFSTarget::Normal - ? Common::FS::GetCitronPath(Common::FS::CitronPath::DumpDir) - : Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir) / "atmosphere" / "contents"; + const auto dump_dir = target == DumpRomFSTarget::Normal + ? Common::FS::GetCitronPath(Common::FS::CitronPath::DumpDir) + : Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir) / + "atmosphere" / "contents"; const auto romfs_dir = fmt::format("{:016X}/romfs", title_id); const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir); @@ -3016,7 +3039,8 @@ void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) { QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this); - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (is_gamescope) { progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint); } @@ -3219,8 +3243,9 @@ bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_vi } // Create icon file path - out_icon_path /= (program_id == 0 ? fmt::format("citron-{}.{}", game_file_name, ico_extension) - : fmt::format("citron-{:016X}.{}", program_id, ico_extension)); + out_icon_path /= + (program_id == 0 ? fmt::format("citron-{}.{}", game_file_name, ico_extension) + : fmt::format("citron-{:016X}.{}", program_id, ico_extension)); return true; } @@ -3324,14 +3349,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga void GMainWindow::OnGameListOpenDirectory(const QString& directory) { std::filesystem::path fs_path; if (directory == QStringLiteral("SDMC")) { - fs_path = - Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir) / "Nintendo/Contents/registered"; + fs_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir) / + "Nintendo/Contents/registered"; } else if (directory == QStringLiteral("UserNAND")) { fs_path = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir) / "user/Contents/registered"; } else if (directory == QStringLiteral("SysNAND")) { - fs_path = - Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir) / "system/Contents/registered"; + fs_path = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir) / + "system/Contents/registered"; } else { fs_path = directory.toStdString(); } @@ -3586,30 +3611,32 @@ void GMainWindow::OnMenuTrimXCI() { if (!trimmer.IsValid()) { QMessageBox::critical(this, tr("Trim XCI File"), - tr("The selected file is not a valid XCI file.")); + tr("The selected file is not a valid XCI file.")); return; } if (!trimmer.CanBeTrimmed()) { - QMessageBox::information(this, tr("Trim XCI File"), - tr("The XCI file does not need to be trimmed (already trimmed or no padding).")); + QMessageBox::information( + this, tr("Trim XCI File"), + tr("The XCI file does not need to be trimmed (already trimmed or no padding).")); return; } // Show confirmation dialog with savings information const double current_size_mb = static_cast(trimmer.GetFileSize()) / (1024.0 * 1024.0); const double data_size_mb = static_cast(trimmer.GetDataSize()) / (1024.0 * 1024.0); - const double savings_mb = static_cast(trimmer.GetDiskSpaceSavings()) / (1024.0 * 1024.0); + const double savings_mb = + static_cast(trimmer.GetDiskSpaceSavings()) / (1024.0 * 1024.0); - const QString info_message = tr( - "This function will check the empty space and then trim the XCI file to save disk space.\n\n" - "Current file size: %1 MB\n" - "Data size: %2 MB\n" - "Potential savings: %3 MB\n\n" - "How would you like to proceed?") - .arg(QString::number(current_size_mb, 'f', 2)) - .arg(QString::number(data_size_mb, 'f', 2)) - .arg(QString::number(savings_mb, 'f', 2)); + const QString info_message = tr("This function will check the empty space and then trim the " + "XCI file to save disk space.\n\n" + "Current file size: %1 MB\n" + "Data size: %2 MB\n" + "Potential savings: %3 MB\n\n" + "How would you like to proceed?") + .arg(QString::number(current_size_mb, 'f', 2)) + .arg(QString::number(data_size_mb, 'f', 2)) + .arg(QString::number(savings_mb, 'f', 2)); // Create custom message box with three options QMessageBox msgBox(this); @@ -3640,8 +3667,7 @@ void GMainWindow::OnMenuTrimXCI() { const QString suggested_name = QDir(file_info.path()).filePath(new_filename); const QString output_filename = QFileDialog::getSaveFileName( - this, tr("Save Trimmed XCI File As"), suggested_name, - tr("NX Cartridge Image (*.xci)")); + this, tr("Save Trimmed XCI File As"), suggested_name, tr("NX Cartridge Image (*.xci)")); if (output_filename.isEmpty()) { return; @@ -3737,39 +3763,41 @@ void GMainWindow::OnMenuTrimXCI() { // Show result if (outcome == Common::XCITrimmer::OperationOutcome::Successful) { // Calculate final size based on whether it was save-as or in-place - const double final_size_mb = is_save_as ? data_size_mb : - static_cast(trimmer.GetFileSize()) / (1024.0 * 1024.0); + const double final_size_mb = + is_save_as ? data_size_mb + : static_cast(trimmer.GetFileSize()) / (1024.0 * 1024.0); const double actual_savings_mb = current_size_mb - final_size_mb; QString success_message; if (is_save_as) { - success_message = tr("Successfully created trimmed XCI file!\n\n" - "Original file: %1\n" - "Original size: %2 MB\n" - "New file: %3\n" - "New size: %4 MB\n" - "Space saved: %5 MB") - .arg(QFileInfo(filename).fileName()) - .arg(QString::number(current_size_mb, 'f', 2)) - .arg(QFileInfo(QString::fromStdString(output_path.string())).fileName()) - .arg(QString::number(final_size_mb, 'f', 2)) - .arg(QString::number(actual_savings_mb, 'f', 2)); + success_message = + tr("Successfully created trimmed XCI file!\n\n" + "Original file: %1\n" + "Original size: %2 MB\n" + "New file: %3\n" + "New size: %4 MB\n" + "Space saved: %5 MB") + .arg(QFileInfo(filename).fileName()) + .arg(QString::number(current_size_mb, 'f', 2)) + .arg(QFileInfo(QString::fromStdString(output_path.string())).fileName()) + .arg(QString::number(final_size_mb, 'f', 2)) + .arg(QString::number(actual_savings_mb, 'f', 2)); } else { success_message = tr("Successfully trimmed XCI file!\n\n" - "Original size: %1 MB\n" - "New size: %2 MB\n" - "Space saved: %3 MB") - .arg(QString::number(current_size_mb, 'f', 2)) - .arg(QString::number(final_size_mb, 'f', 2)) - .arg(QString::number(actual_savings_mb, 'f', 2)); + "Original size: %1 MB\n" + "New size: %2 MB\n" + "Space saved: %3 MB") + .arg(QString::number(current_size_mb, 'f', 2)) + .arg(QString::number(final_size_mb, 'f', 2)) + .arg(QString::number(actual_savings_mb, 'f', 2)); } QMessageBox::information(this, tr("Trim XCI File"), success_message); } else { - const QString error_message = QString::fromStdString( - Common::XCITrimmer::GetOperationOutcomeString(outcome)); + const QString error_message = + QString::fromStdString(Common::XCITrimmer::GetOperationOutcomeString(outcome)); QMessageBox::critical(this, tr("Trim XCI File"), - tr("Failed to trim XCI file: %1").arg(error_message)); + tr("Failed to trim XCI file: %1").arg(error_message)); } } @@ -4023,17 +4051,20 @@ void GMainWindow::OpenURL(const QUrl& url) { void GMainWindow::OnOpenSupport() { QMessageBox::StandardButton first_warning; - first_warning = QMessageBox::question(this, tr("Discord Server Rules"), - tr("WARNING: Before joining the Citron Discord server, you will be required to accept the rules of the server before talking in off-topic channels. Do you understand you must follow & read the #rules upon entering the server?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes); + first_warning = QMessageBox::question( + this, tr("Discord Server Rules"), + tr("WARNING: Before joining the Citron Discord server, you will be required to accept the " + "rules of the server before talking in off-topic channels. Do you understand you must " + "follow & read the #rules upon entering the server?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (first_warning == QMessageBox::Yes) { QMessageBox::StandardButton second_warning; - second_warning = QMessageBox::question(this, tr("Final Confirmation"), - tr("WARNING: Are you sure you understand that you must follow the rules of the Discord before asking for support?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes); + second_warning = + QMessageBox::question(this, tr("Final Confirmation"), + tr("WARNING: Are you sure you understand that you must follow " + "the rules of the Discord before asking for support?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (second_warning == QMessageBox::Yes) { OpenURL(QUrl(QStringLiteral("https://discord.gg/citron"))); @@ -4099,16 +4130,19 @@ void GMainWindow::ShowFullscreen() { } void GMainWindow::HideFullscreen() { - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (ui->action_Single_Window_Mode->isChecked()) { if (UsingExclusiveFullscreen()) { showNormal(); - if (!is_gamescope) restoreGeometry(UISettings::values.geometry); + if (!is_gamescope) + restoreGeometry(UISettings::values.geometry); } else { hide(); setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - if (!is_gamescope) restoreGeometry(UISettings::values.geometry); + if (!is_gamescope) + restoreGeometry(UISettings::values.geometry); raise(); show(); } @@ -4118,11 +4152,13 @@ void GMainWindow::HideFullscreen() { } else { if (UsingExclusiveFullscreen()) { render_window->showNormal(); - if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + if (!is_gamescope) + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } else { render_window->hide(); render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + if (!is_gamescope) + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); render_window->raise(); render_window->show(); } @@ -4158,14 +4194,15 @@ void GMainWindow::ToggleWindowMode() { } void GMainWindow::ResetWindowSize(u32 width, u32 height) { - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; if (is_gamescope) { return; } const auto aspect_ratio = Layout::EmulationAspectRatio( static_cast(Settings::values.aspect_ratio.GetValue()), - static_cast(height) / width); + static_cast(height) / width); if (!ui->action_Single_Window_Mode->isChecked()) { render_window->resize(height / aspect_ratio, height); } else { @@ -4593,7 +4630,12 @@ void GMainWindow::LoadAmiibo(const QString& filename) { void GMainWindow::OnOpenCitronFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile( - QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::CitronDir)))); + QString::fromStdString(Common::FS::GetCitronPath(Common::FS::CitronPath::CitronDir)))); +} + +void GMainWindow::OnOpenLogFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(Common::FS::GetCitronPath(Common::FS::CitronPath::LogDir)))); } void GMainWindow::OnVerifyInstalledContents() { @@ -4626,11 +4668,13 @@ void GMainWindow::OnVerifyInstalledContents() { } } -bool GMainWindow::ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path) { +bool GMainWindow::ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, + const std::filesystem::path& extract_path) { return ExtractZipToDirectory(zip_path, extract_path); } -bool GMainWindow::ExtractZipToDirectory(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path) { +bool GMainWindow::ExtractZipToDirectory(const std::filesystem::path& zip_path, + const std::filesystem::path& extract_path) { #ifdef CITRON_ENABLE_LIBARCHIVE // Use libarchive if available (similar to updater code) struct archive* a = archive_read_new(); @@ -4693,9 +4737,9 @@ bool GMainWindow::ExtractZipToDirectory(const std::filesystem::path& zip_path, c // Windows fallback: use PowerShell Expand-Archive std::filesystem::create_directories(extract_path); - std::string powershell_cmd = "powershell -NoProfile -NonInteractive -Command \"Expand-Archive -Path \\\"" + - zip_path.string() + "\\\" -DestinationPath \\\"" + - extract_path.string() + "\\\" -Force\""; + std::string powershell_cmd = + "powershell -NoProfile -NonInteractive -Command \"Expand-Archive -Path \\\"" + + zip_path.string() + "\\\" -DestinationPath \\\"" + extract_path.string() + "\\\" -Force\""; LOG_INFO(Frontend, "Extracting firmware ZIP with PowerShell: {}", powershell_cmd); @@ -4725,9 +4769,9 @@ void GMainWindow::OnInstallFirmwareFromZip() { // Check for installed keys, error out, suggest restart? if (!ContentManager::AreKeysPresent()) { - QMessageBox::information( - this, tr("Keys not installed"), - tr("Install decryption keys and restart citron before attempting to install firmware.")); + QMessageBox::information(this, tr("Keys not installed"), + tr("Install decryption keys and restart citron before attempting " + "to install firmware.")); return; } @@ -4755,7 +4799,8 @@ void GMainWindow::OnInstallFirmwareFromZip() { QtProgressCallback(100, 5); // Create temporary extraction directory - std::filesystem::path temp_extract_path = std::filesystem::temp_directory_path() / "citron_firmware_temp"; + std::filesystem::path temp_extract_path = + std::filesystem::temp_directory_path() / "citron_firmware_temp"; // Clean up any existing temp directory if (std::filesystem::exists(temp_extract_path)) { @@ -4769,8 +4814,9 @@ void GMainWindow::OnInstallFirmwareFromZip() { if (!ExtractZipToDirectory(firmware_zip_location.toStdString(), temp_extract_path)) { progress.close(); std::filesystem::remove_all(temp_extract_path); - QMessageBox::critical(this, tr("Firmware install failed"), - tr("Failed to extract firmware ZIP file. Make sure the file is a valid ZIP archive.")); + QMessageBox::critical( + this, tr("Firmware install failed"), + tr("Failed to extract firmware ZIP file. Make sure the file is a valid ZIP archive.")); return; } @@ -4792,7 +4838,8 @@ void GMainWindow::OnInstallFirmwareFromZip() { progress.close(); std::filesystem::remove_all(temp_extract_path); QMessageBox::warning(this, tr("Firmware install failed"), - tr("Unable to locate firmware NCA files in the ZIP. Make sure the NCA files are at the root of the ZIP archive.")); + tr("Unable to locate firmware NCA files in the ZIP. Make sure the NCA " + "files are at the root of the ZIP archive.")); return; } @@ -4875,7 +4922,7 @@ void GMainWindow::OnInstallFirmwareFromZip() { progress.close(); QMessageBox::information(this, tr("Firmware installed successfully"), - tr("The firmware has been installed successfully.")); + tr("The firmware has been installed successfully.")); OnCheckFirmwareDecryption(); } @@ -4887,9 +4934,9 @@ void GMainWindow::OnInstallFirmware() { // Check for installed keys, error out, suggest restart? if (!ContentManager::AreKeysPresent()) { - QMessageBox::information( - this, tr("Keys not installed"), - tr("Install decryption keys and restart citron before attempting to install firmware.")); + QMessageBox::information(this, tr("Keys not installed"), + tr("Install decryption keys and restart citron before attempting " + "to install firmware.")); return; } @@ -5208,7 +5255,8 @@ u64 GMainWindow::GetTotalVram() const { Vulkan::RendererVulkan* vulkan_renderer = dynamic_cast(&renderer); if (vulkan_renderer) { VideoCore::RasterizerInterface* rasterizer = vulkan_renderer->ReadRasterizer(); - Vulkan::RasterizerVulkan* vulkan_rasterizer = dynamic_cast(rasterizer); + Vulkan::RasterizerVulkan* vulkan_rasterizer = + dynamic_cast(rasterizer); if (vulkan_rasterizer) { return vulkan_rasterizer->GetTotalVram(); } @@ -5229,7 +5277,8 @@ u64 GMainWindow::GetUsedVram() const { Vulkan::RendererVulkan* vulkan_renderer = dynamic_cast(&renderer); if (vulkan_renderer) { VideoCore::RasterizerInterface* rasterizer = vulkan_renderer->ReadRasterizer(); - Vulkan::RasterizerVulkan* vulkan_rasterizer = dynamic_cast(rasterizer); + Vulkan::RasterizerVulkan* vulkan_rasterizer = + dynamic_cast(rasterizer); if (vulkan_rasterizer) { return vulkan_rasterizer->GetUsedVram(); } @@ -5250,7 +5299,8 @@ u64 GMainWindow::GetBufferMemoryUsage() const { Vulkan::RendererVulkan* vulkan_renderer = dynamic_cast(&renderer); if (vulkan_renderer) { VideoCore::RasterizerInterface* rasterizer = vulkan_renderer->ReadRasterizer(); - Vulkan::RasterizerVulkan* vulkan_rasterizer = dynamic_cast(rasterizer); + Vulkan::RasterizerVulkan* vulkan_rasterizer = + dynamic_cast(rasterizer); if (vulkan_rasterizer) { return vulkan_rasterizer->GetBufferMemoryUsage(); } @@ -5271,7 +5321,8 @@ u64 GMainWindow::GetTextureMemoryUsage() const { Vulkan::RendererVulkan* vulkan_renderer = dynamic_cast(&renderer); if (vulkan_renderer) { VideoCore::RasterizerInterface* rasterizer = vulkan_renderer->ReadRasterizer(); - Vulkan::RasterizerVulkan* vulkan_rasterizer = dynamic_cast(rasterizer); + Vulkan::RasterizerVulkan* vulkan_rasterizer = + dynamic_cast(rasterizer); if (vulkan_rasterizer) { return vulkan_rasterizer->GetTextureMemoryUsage(); } @@ -5292,7 +5343,8 @@ u64 GMainWindow::GetStagingMemoryUsage() const { Vulkan::RendererVulkan* vulkan_renderer = dynamic_cast(&renderer); if (vulkan_renderer) { VideoCore::RasterizerInterface* rasterizer = vulkan_renderer->ReadRasterizer(); - Vulkan::RasterizerVulkan* vulkan_rasterizer = dynamic_cast(rasterizer); + Vulkan::RasterizerVulkan* vulkan_rasterizer = + dynamic_cast(rasterizer); if (vulkan_rasterizer) { return vulkan_rasterizer->GetStagingMemoryUsage(); } @@ -5437,8 +5489,8 @@ void GMainWindow::OnCaptureScreenshot() { const u64 title_id = current_title_id; - const auto screenshot_path = - QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ScreenshotsDir)); + const auto screenshot_path = QString::fromStdString( + Common::FS::GetCitronPathString(Common::FS::CitronPath::ScreenshotsDir)); const auto date = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz")); @@ -5499,12 +5551,12 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie std::string base_title = "citron "; base_title += Common::g_build_fullname; // This is "Nightly " or "" for Stable base_title += "| "; - base_title += Common::g_build_version; // This is the git hash or Stable version tag. + base_title += Common::g_build_version; // This is the git hash or Stable version tag. - // Add the PGO tag if enabled. - #ifdef CITRON_ENABLE_PGO_USE - base_title += " | PGO"; - #endif +// Add the PGO tag if enabled. +#ifdef CITRON_ENABLE_PGO_USE + base_title += " | PGO"; +#endif if (title_name.empty()) { setWindowTitle(QString::fromStdString(base_title)); @@ -5606,8 +5658,8 @@ void GMainWindow::UpdateStatusBar() { if (Settings::values.use_speed_limit.GetValue()) { emu_speed_label->setText(tr("Speed: %1% / %2%") - .arg(results.emulation_speed * 100.0, 0, 'f', 0) - .arg(Settings::values.speed_limit.GetValue())); + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.speed_limit.GetValue())); } else { emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); } @@ -5694,7 +5746,8 @@ void GMainWindow::UpdateStatusButtons() { } void GMainWindow::UpdateUISettings() { - const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + const bool is_gamescope = + !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; // Only save/restore geometry if we are NOT in gamescope to prevent resolution bugs if (!ui->action_Fullscreen->isChecked() && !is_gamescope) { @@ -5703,10 +5756,10 @@ void GMainWindow::UpdateUISettings() { } UISettings::values.state = saveState(); - #if MICROPROFILE_ENABLED +#if MICROPROFILE_ENABLED UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); UISettings::values.microprofile_visible = microProfileDialog->isVisible(); - #endif +#endif UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); @@ -5736,10 +5789,10 @@ void GMainWindow::OnMouseActivity() { void GMainWindow::OnCheckFirmwareDecryption() { system->GetFileSystemController().CreateFactories(*vfs); if (!ContentManager::AreKeysPresent()) { - QMessageBox::warning( - this, tr("Derivation Components Missing"), - tr("Encryption keys are missing. " - "
For support, please visit Help > Get Support (Discord) in the main emulation window.")); + QMessageBox::warning(this, tr("Derivation Components Missing"), + tr("Encryption keys are missing. " + "
For support, please visit Help > Get Support (Discord) " + "in the main emulation window.")); } SetFirmwareVersion(); UpdateMenuState(); @@ -6013,11 +6066,13 @@ void GMainWindow::UpdateUITheme() { QIcon::setThemeName(current_theme); AdjustLinkColor(); #else - bool is_adaptive_theme = (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")); + bool is_adaptive_theme = + (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")); if (is_adaptive_theme) { // For adaptive themes, check the OS state and load the appropriate stylesheet. - QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme : startup_icon_theme); + QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme + : startup_icon_theme); QIcon::setThemeSearchPaths(QStringList(default_theme_paths)); if (CheckDarkMode()) { // If OS is dark, use the dark variant of the adaptive theme. @@ -6195,8 +6250,8 @@ void VolumeButton::ResetMultiplier() { static void SetHighDPIAttributes() { [[maybe_unused]] const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || - qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || - !qgetenv("STEAM_DECK").isEmpty(); + qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || + !qgetenv("STEAM_DECK").isEmpty(); #ifdef _WIN32 // Windows logic: Set policy globally. @@ -6209,7 +6264,7 @@ static void SetHighDPIAttributes() { HMODULE shcore = LoadLibrary(L"shcore.dll"); if (shcore) { - typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int); + typedef HRESULT(WINAPI * SetProcessDpiAwarenessFunc)(int); SetProcessDpiAwarenessFunc setProcessDpiAwareness = (SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness"); if (setProcessDpiAwareness) { @@ -6218,11 +6273,11 @@ static void SetHighDPIAttributes() { FreeLibrary(shcore); } #else -if (is_gamescope) { - // PassThrough prevents Qt6 from recursively expanding layouts to fit rounded DPIs - QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -} + if (is_gamescope) { + // PassThrough prevents Qt6 from recursively expanding layouts to fit rounded DPIs + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + } #endif } @@ -6322,14 +6377,12 @@ int main(int argc, char* argv[]) { #endif if (is_gamescope) { - app.setStyleSheet(app.styleSheet().append(QStringLiteral( - "QDialog { " - " font-size: 11pt; " - " margin: 0px; " - " padding: 0px; " - "}" - "QLabel { font-size: 10pt; }" - ))); + app.setStyleSheet(app.styleSheet().append(QStringLiteral("QDialog { " + " font-size: 11pt; " + " margin: 0px; " + " padding: 0px; " + "}" + "QLabel { font-size: 10pt; }"))); app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); } @@ -6341,7 +6394,8 @@ int main(int argc, char* argv[]) { #endif #ifdef CITRON_USE_AUTO_UPDATER - std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + std::filesystem::path app_dir = + std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); #ifdef _WIN32 // On Windows, updates are applied by the helper script after the app exits. @@ -6349,7 +6403,8 @@ int main(int argc, char* argv[]) { if (std::filesystem::exists(staging_path)) { try { std::filesystem::remove_all(staging_path); - } catch (...) {} + } catch (...) { + } } #else if (Updater::UpdaterService::HasStagedUpdate(app_dir)) { @@ -6387,19 +6442,19 @@ int main(int argc, char* argv[]) { } void GMainWindow::OnCheckForUpdates() { - #ifdef CITRON_USE_AUTO_UPDATER +#ifdef CITRON_USE_AUTO_UPDATER auto* updater_dialog = new Updater::UpdaterDialog(this); updater_dialog->setAttribute(Qt::WA_DeleteOnClose); updater_dialog->show(); updater_dialog->CheckForUpdates(); - #else +#else QMessageBox::information(this, tr("Updates"), tr("The automatic updater is not enabled in this build.")); - #endif +#endif } void GMainWindow::CheckForUpdatesAutomatically() { - #ifdef CITRON_USE_AUTO_UPDATER +#ifdef CITRON_USE_AUTO_UPDATER // Check if automatic updates are enabled in general settings if (!Settings::values.enable_auto_update_check.GetValue()) { return; @@ -6416,8 +6471,12 @@ void GMainWindow::CheckForUpdatesAutomatically() { QMessageBox msg_box(this); msg_box.setWindowTitle(tr("Update Available")); msg_box.setText(tr("A new version of Citron is available: %1") - .arg(QString::fromStdString(update_info.version))); - msg_box.setInformativeText(tr("Click Help → Check for Updates to download it. You can also choose whether you get notified of Stable or Nightly releases. Head over to Emulation -> Configure & go to the UI Tab and choose your selection within the Update Channel.")); + .arg(QString::fromStdString(update_info.version))); + msg_box.setInformativeText( + tr("Click Help → Check for Updates to download it. You can also choose " + "whether you get notified of Stable or Nightly releases. Head over to " + "Emulation -> Configure & go to the UI Tab and choose your selection " + "within the Update Channel.")); msg_box.setIcon(QMessageBox::Information); msg_box.setStandardButtons(QMessageBox::Ok); @@ -6437,17 +6496,18 @@ void GMainWindow::CheckForUpdatesAutomatically() { updater_service->deleteLater(); }); - connect(updater_service, &Updater::UpdaterService::UpdateCompleted, this, - [updater_service](Updater::UpdaterService::UpdateResult result, const QString& message) { - if (result == Updater::UpdaterService::UpdateResult::NetworkError || - result == Updater::UpdaterService::UpdateResult::Failed) { - LOG_WARNING(Frontend, "Automatic update check failed: {}", message.toStdString()); - } - updater_service->deleteLater(); - }); + connect( + updater_service, &Updater::UpdaterService::UpdateCompleted, this, + [updater_service](Updater::UpdaterService::UpdateResult result, const QString& message) { + if (result == Updater::UpdaterService::UpdateResult::NetworkError || + result == Updater::UpdaterService::UpdateResult::Failed) { + LOG_WARNING(Frontend, "Automatic update check failed: {}", message.toStdString()); + } + updater_service->deleteLater(); + }); updater_service->CheckForUpdates(); - #endif +#endif } void GMainWindow::RegisterAutoloaderContents() { @@ -6463,7 +6523,8 @@ void GMainWindow::RegisterAutoloaderContents() { LOG_INFO(Frontend, "Scanning for Autoloader contents..."); for (const auto& title_dir_entry : std::filesystem::directory_iterator(autoloader_root)) { - if (!title_dir_entry.is_directory()) continue; + if (!title_dir_entry.is_directory()) + continue; u64 title_id_val = 0; try { @@ -6473,27 +6534,34 @@ void GMainWindow::RegisterAutoloaderContents() { } const auto it = disabled_addons.find(title_id_val); - const auto& disabled_for_game = (it != disabled_addons.end()) ? it->second : std::vector{}; + const auto& disabled_for_game = + (it != disabled_addons.end()) ? it->second : std::vector{}; const auto process_content_type = [&](const std::filesystem::path& content_path) { - if (!Common::FS::IsDir(content_path)) return; + if (!Common::FS::IsDir(content_path)) + return; for (const auto& mod_dir_entry : std::filesystem::directory_iterator(content_path)) { - if (!mod_dir_entry.is_directory()) continue; + if (!mod_dir_entry.is_directory()) + continue; const std::string mod_name = mod_dir_entry.path().filename().string(); - if (std::find(disabled_for_game.begin(), disabled_for_game.end(), mod_name) != disabled_for_game.end()) { - LOG_INFO(Frontend, "Skipping disabled Autoloader content: {}", mod_name); - continue; - } + // Citron: We do NOT skip disabled content here. + // If we skip it here, it doesn't show up in the UI (Properties -> Add-ons), + // making it impossible for the user to re-enable it. + // The PatchManager (core/file_sys/patch_manager.cpp) handles the actual enforcement + // of disabled status during game load. std::optional cnmt; - for (const auto& file_entry : std::filesystem::directory_iterator(mod_dir_entry.path())) { + for (const auto& file_entry : + std::filesystem::directory_iterator(mod_dir_entry.path())) { if (file_entry.path().string().ends_with(".cnmt.nca")) { - auto vfs_file = vfs->OpenFile(file_entry.path().string(), FileSys::OpenMode::Read); + auto vfs_file = + vfs->OpenFile(file_entry.path().string(), FileSys::OpenMode::Read); if (vfs_file) { FileSys::NCA meta_nca(vfs_file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { + if (meta_nca.GetStatus() == Loader::ResultStatus::Success && + !meta_nca.GetSubdirectories().empty()) { auto section0 = meta_nca.GetSubdirectories()[0]; if (!section0->GetFiles().empty()) { cnmt.emplace(section0->GetFiles()[0]); @@ -6504,14 +6572,16 @@ void GMainWindow::RegisterAutoloaderContents() { } } - if (!cnmt) continue; + if (!cnmt) + continue; for (const auto& record : cnmt->GetContentRecords()) { std::string nca_filename = Common::HexToString(record.nca_id) + ".nca"; std::filesystem::path nca_path = mod_dir_entry.path() / nca_filename; auto nca_vfs_file = vfs->OpenFile(nca_path.string(), FileSys::OpenMode::Read); if (nca_vfs_file) { - autoloader_provider->AddEntry(cnmt->GetType(), record.type, cnmt->GetTitleID(), nca_vfs_file); + autoloader_provider->AddEntry(cnmt->GetType(), record.type, + cnmt->GetTitleID(), nca_vfs_file); } } } @@ -6540,20 +6610,23 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { for (const QString& file : filenames) { QString sanitized_path = file; if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); + sanitized_path = + sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); } auto vfs_file = vfs->OpenFile(sanitized_path.toStdString(), FileSys::OpenMode::Read); if (vfs_file) { FileSys::NSP nsp(vfs_file); if (nsp.GetStatus() == Loader::ResultStatus::Success && !nsp.GetNCAs().empty()) { const auto& [title_id, nca_map] = *nsp.GetNCAs().begin(); - const auto meta_iter = std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair){ - return pair.first.second == FileSys::ContentRecordType::Meta; - }); + const auto meta_iter = + std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair) { + return pair.first.second == FileSys::ContentRecordType::Meta; + }); if (meta_iter != nca_map.end()) { const auto& meta_nca = meta_iter->second; - if (meta_nca && !meta_nca->GetSubdirectories().empty() && !meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { + if (meta_nca && !meta_nca->GetSubdirectories().empty() && + !meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { const auto cnmt_file = meta_nca->GetSubdirectories()[0]->GetFiles()[0]; const FileSys::CNMT cnmt(cnmt_file); if (cnmt.GetType() != FileSys::TitleType::Update) { @@ -6568,7 +6641,8 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { if (dlc_detected) { QMessageBox::warning(this, tr("DLC Detected"), - tr("The Update Manager is not compatible with DLC installations. Please select only update files.")); + tr("The Update Manager is not compatible with DLC installations. " + "Please select only update files.")); return; // Abort the operation. } @@ -6576,7 +6650,8 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { for (const QString& file : filenames) { QString sanitized_path = file; if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); + sanitized_path = + sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); } auto vfs_file = vfs->OpenFile(sanitized_path.toStdString(), FileSys::OpenMode::Read); if (vfs_file) { @@ -6592,7 +6667,8 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { } if (total_size_bytes == 0) { - QMessageBox::warning(this, tr("No files to install"), tr("Could not find any valid files to install in the selected NSPs.")); + QMessageBox::warning(this, tr("No files to install"), + tr("Could not find any valid files to install in the selected NSPs.")); return; } @@ -6616,14 +6692,16 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { QString sanitized_path = file; if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); + sanitized_path = + sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); } const std::string file_path = sanitized_path.toStdString(); LOG_INFO(Loader, "UPDATE MANAGER: Processing sanitized file path: {}", file_path); auto vfs_file = vfs->OpenFile(file_path, FileSys::OpenMode::Read); if (!vfs_file) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at VFS Open. Could not open file: {}", file_path); + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at VFS Open. Could not open file: {}", + file_path); failed_files.append(QFileInfo(file).fileName() + tr(" (File Open Error)")); continue; } @@ -6643,12 +6721,15 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { } const auto& [title_id, nca_map] = *title_map.begin(); - const auto& [type_pair, meta_nca] = *std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair){ - return pair.first.second == FileSys::ContentRecordType::Meta; - }); + const auto& [type_pair, meta_nca] = + *std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair) { + return pair.first.second == FileSys::ContentRecordType::Meta; + }); - if (!meta_nca || meta_nca->GetSubdirectories().empty() || meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at Metadata search for title {}: malformed.", title_id); + if (!meta_nca || meta_nca->GetSubdirectories().empty() || + meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at Metadata search for title {}: malformed.", + title_id); failed_files.append(QFileInfo(file).fileName() + tr(" (Malformed Metadata)")); continue; } @@ -6660,11 +6741,14 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { u64 program_id = FileSys::GetBaseTitleID(title_id); QString nsp_name = QFileInfo(sanitized_path).completeBaseName(); std::string sdmc_path = Common::FS::GetCitronPathString(Common::FS::CitronPath::SDMCDir); - std::string dest_path_str = fmt::format("{}/autoloader/{:016X}/{}/{}", sdmc_path, program_id, type_folder, nsp_name.toStdString()); + std::string dest_path_str = fmt::format("{}/autoloader/{:016X}/{}/{}", sdmc_path, + program_id, type_folder, nsp_name.toStdString()); - auto dest_dir = VfsFilesystemCreateDirectoryWrapper(vfs, dest_path_str, FileSys::OpenMode::ReadWrite); + auto dest_dir = + VfsFilesystemCreateDirectoryWrapper(vfs, dest_path_str, FileSys::OpenMode::ReadWrite); if (!dest_dir) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination directory: {}", dest_path_str); + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination directory: {}", + dest_path_str); failed_files.append(QFileInfo(file).fileName() + tr(" (Directory Creation Error)")); continue; } @@ -6675,13 +6759,15 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { auto dest_file = dest_dir->CreateFileRelative(source_file->GetName()); if (!dest_file) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination file for {}.", source_file->GetName()); + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination file for {}.", + source_file->GetName()); copy_failed = true; break; } if (!dest_file->Resize(source_file->GetSize())) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to resize destination file for {}.", source_file->GetName()); + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to resize destination file for {}.", + source_file->GetName()); copy_failed = true; break; } @@ -6698,7 +6784,8 @@ void GMainWindow::OnMenuInstallWithUpdateManager() { const auto bytes_read = source_file->Read(buffer.data(), bytes_to_read, i); if (bytes_read == 0 && i < source_file->GetSize()) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to read from source file {}.", source_file->GetName()); + LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to read from source file {}.", + source_file->GetName()); copy_failed = true; break; } diff --git a/src/citron/main.h b/src/citron/main.h index f138390d1..38d653526 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -4,23 +4,24 @@ #pragma once +#include #include #include -#include #include #include #include #include #include -#include "common/announce_multiplayer_room.h" -#include "common/common_types.h" -#include "configuration/qt_config.h" -#include "frontend_common/content_manager.h" -#include "input_common/drivers/tas_input.h" #include "citron/compatibility_list.h" #include "citron/hotkeys.h" #include "citron/util/controller_navigation.h" +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "configuration/qt_config.h" #include "core/perf_stats.h" +#include "frontend_common/content_manager.h" +#include "input_common/drivers/tas_input.h" + #ifdef __unix__ #include @@ -61,23 +62,61 @@ class QtControllerSelectorDialog; class QtProfileSelectionDialog; class QtSoftwareKeyboardDialog; class QtNXWebEngineView; -namespace Updater { class UpdaterDialog; } +namespace Updater { +class UpdaterDialog; +} enum class StartGameType { Normal, Global }; -namespace Core { enum class SystemResultStatus : u32; class System; } -namespace Core::Frontend { struct CabinetParameters; struct ControllerParameters; struct InlineAppearParameters; struct InlineTextParameters; struct KeyboardInitializeParameters; struct ProfileSelectParameters; } -namespace DiscordRPC { class DiscordInterface; } -namespace PlayTime { class PlayTimeManager; } -namespace FileSys { class ContentProvider; class ManualContentProvider; class VfsFilesystem; } -namespace InputCommon { class InputSubsystem; } -namespace Service::AM { struct FrontendAppletParameters; enum class AppletId : u32; } -namespace Service::AM::Frontend { enum class SwkbdResult : u32; enum class SwkbdTextCheckResult : u32; enum class SwkbdReplyType : u32; enum class WebExitReason : u32; } -namespace Service::NFC { class NfcDevice; } -namespace Service::NFP { enum class CabinetMode : u8; } -namespace Ui { class MainWindow; } +namespace Core { +enum class SystemResultStatus : u32; +class System; +} // namespace Core +namespace Core::Frontend { +struct CabinetParameters; +struct ControllerParameters; +struct InlineAppearParameters; +struct InlineTextParameters; +struct KeyboardInitializeParameters; +struct ProfileSelectParameters; +} // namespace Core::Frontend +namespace DiscordRPC { +class DiscordInterface; +} +namespace PlayTime { +class PlayTimeManager; +} +namespace FileSys { +class ContentProvider; +class ManualContentProvider; +class VfsFilesystem; +} // namespace FileSys +namespace InputCommon { +class InputSubsystem; +} +namespace Service::AM { +struct FrontendAppletParameters; +enum class AppletId : u32; +} // namespace Service::AM +namespace Service::AM::Frontend { +enum class SwkbdResult : u32; +enum class SwkbdTextCheckResult : u32; +enum class SwkbdReplyType : u32; +enum class WebExitReason : u32; +} // namespace Service::AM::Frontend +namespace Service::NFC { +class NfcDevice; +} +namespace Service::NFP { +enum class CabinetMode : u8; +} +namespace Ui { +class MainWindow; +} enum class EmulatedDirectoryTarget { NAND, SDMC }; -namespace VkDeviceInfo { class Record; } +namespace VkDeviceInfo { +class Record; +} class VolumeButton : public QPushButton { Q_OBJECT @@ -87,10 +126,12 @@ public: } signals: void VolumeChanged(); + protected: void wheelEvent(QWheelEvent* event) override; private slots: void ResetMultiplier(); + private: int scroll_multiplier; QTimer scroll_timer; @@ -102,24 +143,47 @@ class GMainWindow : public QMainWindow { static const int max_recent_files_item = 10; friend class PerformanceOverlay; friend class VramOverlay; - enum { CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, CREATE_SHORTCUT_MSGBOX_SUCCESS, CREATE_SHORTCUT_MSGBOX_ERROR, CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING }; + enum { + CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, + CREATE_SHORTCUT_MSGBOX_SUCCESS, + CREATE_SHORTCUT_MSGBOX_ERROR, + CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING + }; + public: void filterBarSetChecked(bool state); void UpdateUITheme(); - bool IsConfiguring() const { return m_is_configuring; } + bool IsConfiguring() const { + return m_is_configuring; + } explicit GMainWindow(std::unique_ptr config_, bool has_broken_vulkan); ~GMainWindow() override; bool DropAction(QDropEvent* event); void AcceptDropEvent(QDropEvent* event); - MultiplayerState* GetMultiplayerState() { return multiplayer_state; } - Core::System* GetSystem() { return system.get(); } - const std::shared_ptr& GetVFS() const { return vfs; } - bool IsEmulationRunning() const { return emulation_running; } + MultiplayerState* GetMultiplayerState() { + return multiplayer_state; + } + Core::System* GetSystem() { + return system.get(); + } + const std::shared_ptr& GetVFS() const { + return vfs; + } + bool IsEmulationRunning() const { + return emulation_running; + } void RefreshGameList(); - GRenderWindow* GetRenderWindow() const { return render_window; } - bool ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path); - [[nodiscard]] bool HasPerformedInitialSync() const { return has_performed_initial_sync; } - void SetPerformedInitialSync(bool synced) { has_performed_initial_sync = synced; } + GRenderWindow* GetRenderWindow() const { + return render_window; + } + bool ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, + const std::filesystem::path& extract_path); + [[nodiscard]] bool HasPerformedInitialSync() const { + return has_performed_initial_sync; + } + void SetPerformedInitialSync(bool synced) { + has_performed_initial_sync = synced; + } signals: void EmulationStarting(EmuThread* emu_thread); void EmulationStopping(); @@ -130,8 +194,10 @@ signals: void ControllerSelectorReconfigureFinished(bool is_success); void ErrorDisplayFinished(); void ProfileSelectorFinishedSelection(std::optional uuid); - void SoftwareKeyboardSubmitNormalText(Service::AM::Frontend::SwkbdResult result, std::u16string submitted_text, bool confirmed); - void SoftwareKeyboardSubmitInlineText(Service::AM::Frontend::SwkbdReplyType reply_type, std::u16string submitted_text, s32 cursor_position); + void SoftwareKeyboardSubmitNormalText(Service::AM::Frontend::SwkbdResult result, + std::u16string submitted_text, bool confirmed); + void SoftwareKeyboardSubmitInlineText(Service::AM::Frontend::SwkbdReplyType reply_type, + std::u16string submitted_text, s32 cursor_position); void WebBrowserExtractOfflineRomFS(); void WebBrowserClosed(Service::AM::Frontend::WebExitReason exit_reason, std::string last_url); void SigInterrupt(); @@ -141,13 +207,18 @@ public slots: void OnExecuteProgram(std::size_t program_index); void OnExit(); void OnSaveConfig(); - void AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, std::shared_ptr nfp_device); + void AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, + std::shared_ptr nfp_device); void AmiiboSettingsRequestExit(); - void ControllerSelectorReconfigureControllers(const Core::Frontend::ControllerParameters& parameters); + void ControllerSelectorReconfigureControllers( + const Core::Frontend::ControllerParameters& parameters); void ControllerSelectorRequestExit(); - void SoftwareKeyboardInitialize(bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters); + void SoftwareKeyboardInitialize( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters); void SoftwareKeyboardShowNormal(); - void SoftwareKeyboardShowTextCheck(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message); + void SoftwareKeyboardShowTextCheck( + Service::AM::Frontend::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message); void SoftwareKeyboardShowInline(Core::Frontend::InlineAppearParameters appear_parameters); void SoftwareKeyboardHideInline(); void SoftwareKeyboardInlineTextChanged(Core::Frontend::InlineTextParameters text_parameters); @@ -156,13 +227,16 @@ public slots: void ErrorDisplayRequestExit(); void ProfileSelectorSelectProfile(const Core::Frontend::ProfileSelectParameters& parameters); void ProfileSelectorRequestExit(); - void WebBrowserOpenWebPage(const std::string& main_url, const std::string& additional_args, bool is_local); + void WebBrowserOpenWebPage(const std::string& main_url, const std::string& additional_args, + bool is_local); void WebBrowserRequestExit(); void OnAppFocusStateChanged(Qt::ApplicationState state); void OnTasStateChanged(); void IncrementInstallProgress(); + private: - void LinkActionShortcut(QAction* action, const QString& action_name, const bool tas_allowed = false); + void LinkActionShortcut(QAction* action, const QString& action_name, + const bool tas_allowed = false); void RegisterMetaTypes(); void RegisterAutoloaderContents(); void InitializeWidgets(); @@ -177,7 +251,8 @@ private: void PreventOSSleep(); void AllowOSSleep(); bool LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params); - void BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, StartGameType with_config = StartGameType::Normal); + void BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, + StartGameType with_config = StartGameType::Normal); void BootGameFromList(const QString& filename, StartGameType with_config); void ShutdownGame(); void ShowTelemetryCallout(); @@ -192,17 +267,20 @@ private: void RequestGameExit(); void changeEvent(QEvent* event) override; void closeEvent(QCloseEvent* event) override; - std::string CreateTASFramesString(std::array frames) const; - #ifdef __unix__ + std::string CreateTASFramesString( + std::array frames) const; +#ifdef __unix__ void SetupSigInterrupts(); static void HandleSigInterrupt(int); void OnSigInterruptNotifierActivated(); void SetGamemodeEnabled(bool state); - #endif +#endif Core::PerfStatsResults last_perf_stats{}; Service::AM::FrontendAppletParameters ApplicationAppletParameters(); - Service::AM::FrontendAppletParameters LibraryAppletParameters(u64 program_id, Service::AM::AppletId applet_id); - Service::AM::FrontendAppletParameters SystemAppletParameters(u64 program_id, Service::AM::AppletId applet_id); + Service::AM::FrontendAppletParameters LibraryAppletParameters(u64 program_id, + Service::AM::AppletId applet_id); + Service::AM::FrontendAppletParameters SystemAppletParameters(u64 program_id, + Service::AM::AppletId applet_id); void SetupHomeMenuCallback(); std::unique_ptr autoloader_provider; u64 current_title_id{0}; @@ -216,16 +294,20 @@ private slots: void OnMenuReportCompatibility(); void OnOpenSupport(); void OnGameListLoadFile(QString game_path, u64 program_id); - void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, const std::string& game_path); + void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, + const std::string& game_path); void OnTransferableShaderCacheOpenFile(u64 program_id); void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); - void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, const std::string& game_path); + void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, + const std::string& game_path); void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListVerifyIntegrity(const std::string& game_path); void OnGameListCopyTID(u64 program_id); - void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); - void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target); + void OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list); + void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target); void OnGameListOpenDirectory(const QString& directory); void OnGameListAddDirectory(); void OnGameListShowList(bool show); @@ -252,10 +334,12 @@ private slots: void OnConfigurePerGame(); void OnLoadAmiibo(); void OnOpenCitronFolder(); + void OnOpenLogFolder(); void OnVerifyInstalledContents(); void OnInstallFirmware(); void OnInstallFirmwareFromZip(); - bool ExtractZipToDirectory(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path); + bool ExtractZipToDirectory(const std::filesystem::path& zip_path, + const std::filesystem::path& extract_path); void OnInstallDecryptionKeys(); void OnAbout(); void OnCheckForUpdates(); @@ -300,6 +384,7 @@ private slots: void OnShutdownBeginDialog(); void OnEmulationStopped(); void OnEmulationStopTimeExpired(); + private: QString GetGameListErrorRemoving(InstalledEntryType type) const; void RemoveBaseContent(u64 program_id, InstalledEntryType type); @@ -311,10 +396,12 @@ private: void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); void RemovePlayTimeData(u64 program_id); void RemoveCacheStorage(u64 program_id); - bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, u64* selected_title_id, u8* selected_content_record_type); + bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, + u64* selected_title_id, u8* selected_content_record_type); ContentManager::InstallResult InstallNCA(const QString& filename); void MigrateConfigFiles(); - void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, std::string_view gpu_vendor = {}); + void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, + std::string_view gpu_vendor = {}); void UpdateDockedButton(); void UpdateAPIText(); void UpdateFilterText(); @@ -337,9 +424,17 @@ private: bool ConfirmShutdownGame(); QString GetTasStateDescription() const; bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title); - bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, std::filesystem::path& out_icon_path); - bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, const std::filesystem::path& icon_path, const std::filesystem::path& command, const std::string& arguments, const std::string& categories, const std::string& keywords, const std::string& name); - bool question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path); + bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, const std::string& arguments, + const std::string& categories, const std::string& keywords, + const std::string& name); + bool question(QWidget* parent, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = + QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); std::unique_ptr system; std::shared_ptr input_subsystem; std::unique_ptr ui; @@ -410,10 +505,10 @@ private: bool m_is_updating_theme = false; bool m_is_configuring = false; bool has_performed_initial_sync = false; - #ifdef __unix__ +#ifdef __unix__ QSocketNotifier* sig_interrupt_notifier; static std::array sig_interrupt_fds; - #endif +#endif protected: void dropEvent(QDropEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; diff --git a/src/citron/main.ui b/src/citron/main.ui index fd9e7c345..69f1dbbf8 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -192,6 +192,8 @@ + + @@ -450,6 +452,11 @@ Open &citron Folder + + + Open &Log Folder + + false diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 190ee1021..7f6682202 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -459,6 +459,8 @@ add_library(core STATIC hle/service/am/process_holder.h hle/service/am/service/all_system_applet_proxies_service.cpp hle/service/am/service/all_system_applet_proxies_service.h + hle/service/am/service/applet_alternative_functions.cpp + hle/service/am/service/applet_alternative_functions.h hle/service/am/service/applet_common_functions.cpp hle/service/am/service/applet_common_functions.h hle/service/am/service/application_accessor.cpp @@ -499,6 +501,8 @@ add_library(core STATIC hle/service/am/service/lock_accessor.h hle/service/am/service/overlay_applet_proxy.cpp hle/service/am/service/overlay_applet_proxy.h + hle/service/am/service/overlay_functions.cpp + hle/service/am/service/overlay_functions.h hle/service/am/service/process_winding_controller.cpp hle/service/am/service/process_winding_controller.h hle/service/am/service/self_controller.cpp @@ -1257,7 +1261,8 @@ target_link_libraries(core nlohmann_json::nlohmann_json mbedtls RenderDoc::API -) + stb::headers + ) # Conditionally link against Boost::process ONLY if it was found by the main CMakeLists.txt. if(Boost_PROCESS_FOUND) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 2bfe34689..5a858d433 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -373,6 +373,12 @@ std::optional RegisteredCache::GetNcaIDFromMetadata(u64 title_id, const auto res1 = CheckMapForContentRecord(citron_meta, title_id, type); if (res1) return res1; + const auto res2 = CheckMapForContentRecord(legacy_meta, title_id, type); + if (res2) { + LOG_INFO(Loader, "Found content {:016X} type {:02X} in legacy_meta", title_id, + static_cast(type)); + return res2; + } return CheckMapForContentRecord(meta, title_id, type); } @@ -461,6 +467,7 @@ void RegisteredCache::Refresh() { const auto ids = AccumulateFiles(); ProcessFiles(ids); AccumulateCitronMeta(); + AccumulateLegacyMeta(); } RegisteredCache::RegisteredCache(VirtualDir dir_, ContentProviderParsingFunction parsing_function) @@ -490,6 +497,11 @@ std::optional RegisteredCache::GetEntryVersion(u64 title_id) const { return citron_meta_iter->second.GetTitleVersion(); } + const auto legacy_meta_iter = legacy_meta.find(title_id); + if (legacy_meta_iter != legacy_meta.cend()) { + return legacy_meta_iter->second.GetTitleVersion(); + } + return std::nullopt; } @@ -527,6 +539,14 @@ void RegisteredCache::IterateAllMetadata( } } } + for (const auto& kv : legacy_meta) { + const auto& cnmt = kv.second; + for (const auto& rec : cnmt.GetContentRecords()) { + if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { + out.push_back(proc(cnmt, rec)); + } + } + } } std::vector RegisteredCache::ListEntriesFilter( @@ -758,6 +778,15 @@ bool RegisteredCache::RemoveExistingEntry(u64 title_id) const { } } + // If patch entries for any program exist in yuzu meta, remove them + for (u8 i = 0; i < 0x10; i++) { + const auto meta_dir = dir->CreateDirectoryRelative("yuzu_meta"); + const auto filename = GetCNMTName(TitleType::Update, title_id + i); + if (meta_dir->GetFile(filename)) { + removed_data |= meta_dir->DeleteFile(filename); + } + } + return removed_data; } @@ -790,7 +819,9 @@ InstallResult RegisteredCache::RawInstallNCA(const NCA& nca, const VfsCopyFuncti if (GetFileAtID(id) != nullptr) { LOG_WARNING(Loader, "Overwriting existing NCA..."); VirtualDir c_dir; - { c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); } + { + c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); + } c_dir->DeleteFile(Common::FS::GetFilename(path)); } @@ -1033,4 +1064,36 @@ std::vector ManualContentProvider::ListEntriesFilter( out.erase(std::unique(out.begin(), out.end()), out.end()); return out; } + +void RegisteredCache::AccumulateLegacyMeta() { + LOG_INFO(Loader, "AccumulateLegacyMeta: Scanning directory '{}' for yuzu_meta", + dir->GetFullPath()); + + const auto meta_dir = dir->GetSubdirectory("yuzu_meta"); + if (meta_dir == nullptr) { + LOG_INFO(Loader, "AccumulateLegacyMeta: yuzu_meta directory not found in '{}'", + dir->GetFullPath()); + return; + } + LOG_INFO(Loader, "Accumulating legacy meta from yuzu_meta at '{}'", meta_dir->GetFullPath()); + + const auto files = meta_dir->GetFiles(); + LOG_INFO(Loader, "yuzu_meta contains {} files", files.size()); + + for (const auto& file : files) { + LOG_INFO(Loader, "Scanning file: name={} extension={}", file->GetName(), + file->GetExtension()); + if (file->GetExtension() != "cnmt") { + continue; + } + + CNMT cnmt(file); + LOG_INFO(Loader, "Loaded legacy CNMT: {:016X}", cnmt.GetTitleID()); + legacy_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt)); + } + + const auto subdirs = meta_dir->GetSubdirectories(); + LOG_INFO(Loader, "yuzu_meta contains {} subdirectories", subdirs.size()); +} + } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index ffcc9ea18..1d188178e 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -165,8 +165,8 @@ public: const VfsCopyFunction& copy = &VfsRawCopy); // Due to the fact that we must use Meta-type NCAs to determine the existence of files, this - // poses quite a challenge. Instead of creating a new meta NCA for this file, citron will create a - // dir inside the NAND called 'citron_meta' and store the raw CNMT there. + // poses quite a challenge. Instead of creating a new meta NCA for this file, citron will create + // a dir inside the NAND called 'citron_meta' and store the raw CNMT there. // TODO(DarkLordZach): Author real meta-type NCAs and install those. InstallResult InstallEntry(const NCA& nca, TitleType type, bool overwrite_if_exists = false, const VfsCopyFunction& copy = &VfsRawCopy); @@ -186,6 +186,7 @@ private: std::vector AccumulateFiles() const; void ProcessFiles(const std::vector& ids); void AccumulateCitronMeta(); + void AccumulateLegacyMeta(); std::optional GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const; VirtualFile GetFileAtID(NcaID id) const; VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& open_dir, std::string_view path) const; @@ -202,6 +203,8 @@ private: std::map meta; // maps tid -> meta for CNMT in citron_meta std::map citron_meta; + // maps tid -> meta for CNMT in yuzu_meta (legacy) + std::map legacy_meta; }; enum class ContentProviderUnionSlot { @@ -209,7 +212,8 @@ enum class ContentProviderUnionSlot { UserNAND, ///< User NAND SDMC, ///< SD Card FrontendManual, ///< Frontend-defined game list or similar - Autoloader, ///< Separate functionality for multiple Updates/DLCs without being overwritten by NAND. + Autoloader, ///< Separate functionality for multiple Updates/DLCs without being overwritten by + ///< NAND. }; // Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface. diff --git a/src/core/hle/kernel/svc/svc_synchronization.cpp b/src/core/hle/kernel/svc/svc_synchronization.cpp index d35f6a2e6..48689f7c3 100644 --- a/src/core/hle/kernel/svc/svc_synchronization.cpp +++ b/src/core/hle/kernel/svc/svc_synchronization.cpp @@ -60,13 +60,13 @@ Result ResetSignal(Core::System& system, Handle handle) { } } - // Handle not found - log once and return success to prevent infinite loops + // Handle not found if (should_log) { - LOG_WARNING(Kernel_SVC, "ResetSignal called with invalid handle 0x{:08X}, returning success to prevent hang", handle); + LOG_WARNING(Kernel_SVC, "ResetSignal called with invalid handle 0x{:08X}", handle); logged_handles.insert(handle); } - R_SUCCEED(); // Return success instead of throwing to prevent infinite loops + R_RETURN(ResultInvalidHandle); } /// Wait for the given handles to synchronize, timeout after the specified nanoseconds diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp index acd5ec7b4..67997b390 100644 --- a/src/core/hle/service/acc/acc.cpp +++ b/src/core/hle/service/acc/acc.cpp @@ -32,6 +32,7 @@ #include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/glue/glue_manager.h" +#include "core/hle/service/ns/ns_types.h" #include "core/hle/service/server_manager.h" #include "core/loader/loader.h" @@ -108,6 +109,7 @@ public: {150, nullptr, "CreateAuthorizationRequest"}, {160, nullptr, "RequiresUpdateNetworkServiceAccountIdTokenCache"}, {161, nullptr, "RequireReauthenticationOfNetworkServiceAccount"}, + {143, D<&IManagerForSystemService::GetNetworkServiceLicenseCacheEx>, "GetNetworkServiceLicenseCacheEx"}, // 15.0.0+ }; // clang-format on @@ -136,6 +138,15 @@ private: R_SUCCEED(); } + Result GetNetworkServiceLicenseCacheEx(Out out_license, Out out_expiration) { + LOG_INFO(Service_ACC, "called"); + + *out_license = 0; + *out_expiration = 0; + + R_SUCCEED(); + } + Common::UUID account_id; }; @@ -333,6 +344,9 @@ public: {1, &IProfileCommon::GetBase, "GetBase"}, {10, &IProfileCommon::GetImageSize, "GetImageSize"}, {11, &IProfileCommon::LoadImage, "LoadImage"}, + {20, &IProfileCommon::Unknown20, "Unknown20"}, + {21, &IProfileCommon::Unknown21, "Unknown21"}, + {30, &IProfileCommon::Unknown30, "Unknown30"}, }; RegisterHandlers(functions); @@ -341,6 +355,7 @@ public: static const FunctionInfo editor_functions[] = { {100, &IProfileCommon::Store, "Store"}, {101, &IProfileCommon::StoreWithImage, "StoreWithImage"}, + {110, &IProfileCommon::Unknown110, "Unknown110"}, }; RegisterHandlers(editor_functions); @@ -432,6 +447,34 @@ protected: rb.Push(static_cast(buffer.size())); } + void Unknown20(HLERequestContext& ctx) { + LOG_DEBUG(Service_ACC, "(STUBBED) called."); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void Unknown21(HLERequestContext& ctx) { + LOG_DEBUG(Service_ACC, "(STUBBED) called."); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void Unknown30(HLERequestContext& ctx) { + LOG_DEBUG(Service_ACC, "(STUBBED) called."); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void Unknown110(HLERequestContext& ctx) { + LOG_DEBUG(Service_ACC, "(STUBBED) called."); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + void Store(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto base = rp.PopRaw(); @@ -602,7 +645,8 @@ protected: class CheckNetworkServiceAvailabilityAsyncInterface final : public IAsyncContext { public: - explicit CheckNetworkServiceAvailabilityAsyncInterface(Core::System& system_) : IAsyncContext{system_} { + explicit CheckNetworkServiceAvailabilityAsyncInterface(Core::System& system_) + : IAsyncContext{system_} { MarkComplete(); } ~CheckNetworkServiceAvailabilityAsyncInterface() = default; @@ -621,7 +665,8 @@ protected: class EnsureSignedDeviceIdentifierCacheAsyncInterface final : public IAsyncContext { public: - explicit EnsureSignedDeviceIdentifierCacheAsyncInterface(Core::System& system_) : IAsyncContext{system_} { + explicit EnsureSignedDeviceIdentifierCacheAsyncInterface(Core::System& system_) + : IAsyncContext{system_} { MarkComplete(); } ~EnsureSignedDeviceIdentifierCacheAsyncInterface() = default; @@ -659,7 +704,8 @@ protected: class SynchronizeNetworkServiceAccountsSnapshotAsyncInterface final : public IAsyncContext { public: - explicit SynchronizeNetworkServiceAccountsSnapshotAsyncInterface(Core::System& system_) : IAsyncContext{system_} { + explicit SynchronizeNetworkServiceAccountsSnapshotAsyncInterface(Core::System& system_) + : IAsyncContext{system_} { MarkComplete(); } ~SynchronizeNetworkServiceAccountsSnapshotAsyncInterface() = default; @@ -1302,7 +1348,8 @@ void Module::Interface::ActivateOpenContextRetention(HLERequestContext& ctx) { rb.PushIpcInterface(system, dummy_uuid); } -void Module::Interface::EnsureSignedDeviceIdentifierCacheForNintendoAccountAsync(HLERequestContext& ctx) { +void Module::Interface::EnsureSignedDeviceIdentifierCacheForNintendoAccountAsync( + HLERequestContext& ctx) { LOG_WARNING(Service_ACC, "(STUBBED) called"); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; @@ -1366,7 +1413,8 @@ void Module::Interface::SetUserPosition(HLERequestContext& ctx) { const auto position = rp.Pop(); const auto uuid = rp.PopRaw(); - LOG_WARNING(Service_ACC, "(STUBBED) called, position={}, uuid=0x{}", position, uuid.RawString()); + LOG_WARNING(Service_ACC, "(STUBBED) called, position={}, uuid=0x{}", position, + uuid.RawString()); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -1429,7 +1477,8 @@ void Module::Interface::ResumeProcedureToCreateUserWithNintendoAccount(HLEReques rb.PushIpcInterface(system, dummy_uuid); } -void Module::Interface::ResumeProcedureToCreateUserWithNintendoAccountAfterApplyResponse(HLERequestContext& ctx) { +void Module::Interface::ResumeProcedureToCreateUserWithNintendoAccountAfterApplyResponse( + HLERequestContext& ctx) { LOG_WARNING(Service_ACC, "(STUBBED) called"); const Common::UUID dummy_uuid{}; @@ -1474,7 +1523,8 @@ void Module::Interface::ProxyProcedureForGuestLoginWithNintendoAccount(HLEReques rb.PushIpcInterface(system, dummy_uuid); } -void Module::Interface::ProxyProcedureForFloatingRegistrationWithNintendoAccount(HLERequestContext& ctx) { +void Module::Interface::ProxyProcedureForFloatingRegistrationWithNintendoAccount( + HLERequestContext& ctx) { LOG_WARNING(Service_ACC, "(STUBBED) called"); const Common::UUID dummy_uuid{}; @@ -1483,7 +1533,8 @@ void Module::Interface::ProxyProcedureForFloatingRegistrationWithNintendoAccount rb.PushIpcInterface(system, dummy_uuid); } -void Module::Interface::ProxyProcedureForDeviceMigrationAuthenticatingOperatingUser(HLERequestContext& ctx) { +void Module::Interface::ProxyProcedureForDeviceMigrationAuthenticatingOperatingUser( + HLERequestContext& ctx) { LOG_WARNING(Service_ACC, "(STUBBED) called"); const Common::UUID dummy_uuid{}; @@ -1591,8 +1642,8 @@ void Module::Interface::DebugSetUserStateOpen(HLERequestContext& ctx) { Module::Interface::Interface(std::shared_ptr module_, std::shared_ptr profile_manager_, Core::System& system_, const char* name) - : ServiceFramework{system_, name}, module{std::move(module_)}, profile_manager{std::move( - profile_manager_)} {} + : ServiceFramework{system_, name}, module{std::move(module_)}, + profile_manager{std::move(profile_manager_)} {} Module::Interface::~Interface() = default; @@ -1605,18 +1656,17 @@ void LoopProcess(Core::System& system) { std::make_shared(module, profile_manager, system)); server_manager->RegisterNamedService("acc:e", std::make_shared(module, profile_manager, system)); - server_manager->RegisterNamedService("acc:e:u1", - std::make_shared(module, profile_manager, system)); - server_manager->RegisterNamedService("acc:e:u2", - std::make_shared(module, profile_manager, system)); + server_manager->RegisterNamedService( + "acc:e:u1", std::make_shared(module, profile_manager, system)); + server_manager->RegisterNamedService( + "acc:e:u2", std::make_shared(module, profile_manager, system)); server_manager->RegisterNamedService("acc:su", std::make_shared(module, profile_manager, system)); server_manager->RegisterNamedService("acc:u0", std::make_shared(module, profile_manager, system)); server_manager->RegisterNamedService("acc:u1", std::make_shared(module, profile_manager, system)); - server_manager->RegisterNamedService("dauth:0", - std::make_shared(system)); + server_manager->RegisterNamedService("dauth:0", std::make_shared(system)); ServerManager::RunServer(std::move(server_manager)); } diff --git a/src/core/hle/service/am/am_types.h b/src/core/hle/service/am/am_types.h index eb9ad0ac5..9ec6ea1a3 100644 --- a/src/core/hle/service/am/am_types.h +++ b/src/core/hle/service/am/am_types.h @@ -15,6 +15,7 @@ class FrontendApplet; enum class AppletType { Application, LibraryApplet, + OverlayApplet, SystemApplet, }; @@ -212,7 +213,7 @@ struct AppletIdentityInfo { }; static_assert(sizeof(AppletIdentityInfo) == 0x10, "AppletIdentityInfo has incorrect size."); -struct AppletAttribute { +struct alignas(8) AppletAttribute { u8 flag; INSERT_PADDING_BYTES_NOINIT(0x7F); }; diff --git a/src/core/hle/service/am/applet.h b/src/core/hle/service/am/applet.h index 571904fab..2522f2577 100644 --- a/src/core/hle/service/am/applet.h +++ b/src/core/hle/service/am/applet.h @@ -113,6 +113,7 @@ struct Applet { bool is_activity_runnable{}; bool is_interactible{true}; bool window_visible{true}; + bool overlay_in_foreground{}; // Events Event gpu_error_detected_event; diff --git a/src/core/hle/service/am/applet_manager.cpp b/src/core/hle/service/am/applet_manager.cpp index 7b5bdf2b3..2ed79880a 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -6,6 +6,7 @@ #include "core/core.h" #include "core/core_timing.h" #include "core/hle/service/acc/profile_manager.h" +#include "core/hle/service/am/process_creation.h" #include "core/hle/service/am/applet_data_broker.h" #include "core/hle/service/am/applet_manager.h" #include "core/hle/service/am/frontend/applet_cabinet.h" @@ -262,6 +263,22 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { m_cv.wait(lk, [&] { return m_pending_process != nullptr; }); + if (true && m_window_system->GetOverlayDisplayApplet() == nullptr) { + if (auto overlay_process = CreateProcess(m_system, static_cast(AppletProgramId::OverlayDisplay), 0, 0)) { + auto overlay_applet = std::make_shared(m_system, std::move(overlay_process), false); + overlay_applet->program_id = static_cast(AppletProgramId::OverlayDisplay); + overlay_applet->applet_id = AppletId::OverlayDisplay; + overlay_applet->type = AppletType::OverlayApplet; + overlay_applet->library_applet_mode = LibraryAppletMode::PartialForeground; + overlay_applet->window_visible = true; + overlay_applet->home_button_short_pressed_blocked = false; + overlay_applet->home_button_long_pressed_blocked = false; + m_window_system->TrackApplet(overlay_applet, false); + overlay_applet->process->Run(); + LOG_INFO(Service_AM, "called, Overlay applet launched before application (initially hidden, watching home button)"); + } + } + const auto& params = m_pending_parameters; auto applet = std::make_shared(m_system, std::move(m_pending_process), params.applet_id == AppletId::Application); diff --git a/src/core/hle/service/am/applet_manager.h b/src/core/hle/service/am/applet_manager.h index 62a797056..893de2eb7 100644 --- a/src/core/hle/service/am/applet_manager.h +++ b/src/core/hle/service/am/applet_manager.h @@ -46,6 +46,10 @@ public: void OperationModeChanged(); public: + WindowSystem* GetWindowSystem() const { + return m_window_system; + } + void SetWindowSystem(WindowSystem* window_system); void SetHomeMenuRequestCallback(std::function callback); diff --git a/src/core/hle/service/am/display_layer_manager.cpp b/src/core/hle/service/am/display_layer_manager.cpp index 85ff6fb88..939952ee3 100644 --- a/src/core/hle/service/am/display_layer_manager.cpp +++ b/src/core/hle/service/am/display_layer_manager.cpp @@ -69,6 +69,17 @@ Result DisplayLayerManager::CreateManagedDisplayLayer(u64* out_layer_id) { out_layer_id, 0, display_id, Service::AppletResourceUserId{m_process->GetProcessId()})); m_manager_display_service->SetLayerVisibility(m_visible, *out_layer_id); + + if (m_applet_id != AppletId::Application) { + (void)m_manager_display_service->SetLayerBlending(m_blending_enabled, *out_layer_id); + if (m_applet_id == AppletId::OverlayDisplay) { + (void)m_manager_display_service->SetLayerZIndex(-1, *out_layer_id); + (void)m_display_service->GetContainer()->SetLayerIsOverlay(*out_layer_id, true); + } else { + (void)m_manager_display_service->SetLayerZIndex(1, *out_layer_id); + } + } + m_managed_display_layers.emplace(*out_layer_id); R_SUCCEED(); @@ -105,7 +116,16 @@ Result DisplayLayerManager::IsSystemBufferSharingEnabled() { // We succeeded, so set up remaining state. m_buffer_sharing_enabled = true; + + // Ensure the overlay layer is visible m_manager_display_service->SetLayerVisibility(m_visible, m_system_shared_layer_id); + m_manager_display_service->SetLayerBlending(m_blending_enabled, m_system_shared_layer_id); + s32 initial_z = 1; + if (m_applet_id == AppletId::OverlayDisplay) { + initial_z = -1; + (void)m_display_service->GetContainer()->SetLayerIsOverlay(m_system_shared_layer_id, true); + } + m_manager_display_service->SetLayerZIndex(initial_z, m_system_shared_layer_id); R_SUCCEED(); } @@ -128,10 +148,14 @@ void DisplayLayerManager::SetWindowVisibility(bool visible) { if (m_manager_display_service) { if (m_system_shared_layer_id) { + LOG_INFO(Service_VI, "shared_layer={} visible={} applet_id={}", + m_system_shared_layer_id, m_visible, static_cast(m_applet_id)); m_manager_display_service->SetLayerVisibility(m_visible, m_system_shared_layer_id); } for (const auto layer_id : m_managed_display_layers) { + LOG_INFO(Service_VI, "managed_layer={} visible={} applet_id={}", layer_id, m_visible, + static_cast(m_applet_id)); m_manager_display_service->SetLayerVisibility(m_visible, layer_id); } } @@ -141,6 +165,22 @@ bool DisplayLayerManager::GetWindowVisibility() const { return m_visible; } +void DisplayLayerManager::SetOverlayZIndex(s32 z_index) { + if (!m_manager_display_service) { + return; + } + + if (m_system_shared_layer_id) { + m_manager_display_service->SetLayerZIndex(z_index, m_system_shared_layer_id); + LOG_INFO(Service_VI, "called, shared_layer={} z={}", m_system_shared_layer_id, z_index); + } + + for (const auto layer_id : m_managed_display_layers) { + m_manager_display_service->SetLayerZIndex(z_index, layer_id); + LOG_INFO(Service_VI, "called, managed_layer={} z={}", layer_id, z_index); + } +} + Result DisplayLayerManager::WriteAppletCaptureBuffer(bool* out_was_written, s32* out_fbshare_layer_index) { R_UNLESS(m_buffer_sharing_enabled, VI::ResultPermissionDenied); diff --git a/src/core/hle/service/am/display_layer_manager.h b/src/core/hle/service/am/display_layer_manager.h index a66509c04..5b44e26d9 100644 --- a/src/core/hle/service/am/display_layer_manager.h +++ b/src/core/hle/service/am/display_layer_manager.h @@ -43,6 +43,8 @@ public: void SetWindowVisibility(bool visible); bool GetWindowVisibility() const; + void SetOverlayZIndex(s32 z_index); + Result WriteAppletCaptureBuffer(bool* out_was_written, s32* out_fbshare_layer_index); private: diff --git a/src/core/hle/service/am/frontend/applet_general.cpp b/src/core/hle/service/am/frontend/applet_general.cpp index d2cabb7b5..72cfe67e1 100644 --- a/src/core/hle/service/am/frontend/applet_general.cpp +++ b/src/core/hle/service/am/frontend/applet_general.cpp @@ -190,6 +190,10 @@ void PhotoViewer::Execute() { case PhotoViewerAppletMode::AllApps: frontend.ShowAllPhotos(callback); break; + case PhotoViewerAppletMode::ShowAllFiles: + // For now, treat ShowAllFiles the same as AllApps in the HLE stub + frontend.ShowAllPhotos(callback); + break; default: UNIMPLEMENTED_MSG("Unimplemented PhotoViewer applet mode={:02X}!", mode); break; diff --git a/src/core/hle/service/am/frontend/applet_general.h b/src/core/hle/service/am/frontend/applet_general.h index eaa7ae25f..2ef30988e 100644 --- a/src/core/hle/service/am/frontend/applet_general.h +++ b/src/core/hle/service/am/frontend/applet_general.h @@ -46,6 +46,7 @@ private: enum class PhotoViewerAppletMode : u8 { CurrentApp = 0, AllApps = 1, + ShowAllFiles = 2, }; class PhotoViewer final : public FrontendApplet { diff --git a/src/core/hle/service/am/service/all_system_applet_proxies_service.cpp b/src/core/hle/service/am/service/all_system_applet_proxies_service.cpp index 475959c18..aefd736f4 100644 --- a/src/core/hle/service/am/service/all_system_applet_proxies_service.cpp +++ b/src/core/hle/service/am/service/all_system_applet_proxies_service.cpp @@ -5,6 +5,7 @@ #include "core/core.h" #include "core/hle/service/am/applet_manager.h" #include "core/hle/service/am/service/all_system_applet_proxies_service.h" +#include "core/hle/service/am/service/applet_alternative_functions.h" #include "core/hle/service/am/service/debug_functions.h" #include "core/hle/service/am/service/library_applet_creator.h" #include "core/hle/service/am/service/library_applet_proxy.h" @@ -31,7 +32,7 @@ IAllSystemAppletProxiesService::IAllSystemAppletProxiesService(Core::System& sys {400, D<&IAllSystemAppletProxiesService::CreateSelfLibraryAppletCreatorForDevelop>, "CreateSelfLibraryAppletCreatorForDevelop"}, {410, D<&IAllSystemAppletProxiesService::GetSystemAppletControllerForDebug>, "GetSystemAppletControllerForDebug"}, {450, D<&IAllSystemAppletProxiesService::GetSystemProcessCommonFunctions>, "GetSystemProcessCommonFunctions"}, - {460, D<&IAllSystemAppletProxiesService::Cmd460>, "Cmd460"}, + {460, D<&IAllSystemAppletProxiesService::GetAppletAlternativeFunctions>, "GetAppletAlternativeFunctions"}, {1000, D<&IAllSystemAppletProxiesService::GetDebugFunctions>, "GetDebugFunctions"}, }; // clang-format on @@ -73,9 +74,8 @@ Result IAllSystemAppletProxiesService::OpenHomeMenuProxy( Result IAllSystemAppletProxiesService::OpenLibraryAppletProxy( Out> out_library_applet_proxy, ClientProcessId pid, - InCopyHandle process_handle, - InLargeData attribute) { - LOG_DEBUG(Service_AM, "called"); + InCopyHandle process_handle, AppletAttribute attribute) { + LOG_WARNING(Service_AM, "called"); if (const auto applet = this->GetAppletFromProcessId(pid); applet) { *out_library_applet_proxy = std::make_shared( @@ -104,8 +104,7 @@ std::shared_ptr IAllSystemAppletProxiesService::GetAppletFromProcessId( Result IAllSystemAppletProxiesService::OpenOverlayAppletProxy( Out> out_overlay_applet_proxy, ClientProcessId pid, - InCopyHandle process_handle, - InLargeData attribute) { + InCopyHandle process_handle, AppletAttribute attribute) { LOG_DEBUG(Service_AM, "called"); if (const auto applet = GetAppletFromProcessId(pid)) { *out_overlay_applet_proxy = std::make_shared( @@ -137,7 +136,8 @@ Result IAllSystemAppletProxiesService::CreateSelfLibraryAppletCreatorForDevelop( LOG_WARNING(Service_AM, "(STUBBED) called"); if (const auto applet = this->GetAppletFromProcessId(pid); applet) { - *out_library_applet_creator = std::make_shared(system, applet, m_window_system); + *out_library_applet_creator = + std::make_shared(system, applet, m_window_system); R_SUCCEED(); } else { R_THROW(ResultUnknown); @@ -156,8 +156,11 @@ Result IAllSystemAppletProxiesService::GetSystemProcessCommonFunctions( R_SUCCEED(); } -Result IAllSystemAppletProxiesService::Cmd460() { - LOG_WARNING(Service_AM, "(STUBBED) called"); +Result IAllSystemAppletProxiesService::GetAppletAlternativeFunctions( + Out> out_applet_alternative_functions) { + LOG_WARNING(Service_AM, "called"); + *out_applet_alternative_functions = + std::make_shared(system, m_window_system); R_SUCCEED(); } diff --git a/src/core/hle/service/am/service/all_system_applet_proxies_service.h b/src/core/hle/service/am/service/all_system_applet_proxies_service.h index 72c9252ba..a70054c49 100644 --- a/src/core/hle/service/am/service/all_system_applet_proxies_service.h +++ b/src/core/hle/service/am/service/all_system_applet_proxies_service.h @@ -4,6 +4,7 @@ #pragma once +#include "core/hle/service/am/am_types.h" #include "core/hle/service/cmif_types.h" #include "core/hle/service/service.h" @@ -19,7 +20,9 @@ class ILibraryAppletProxy; class IOverlayAppletProxy; class ISystemAppletProxy; class ISystemApplicationProxy; +class ISystemApplicationProxy; class ISystemProcessCommonFunctions; +class IAppletAlternativeFunctions; class WindowSystem; class IAllSystemAppletProxiesService final @@ -33,29 +36,29 @@ private: ClientProcessId pid, InCopyHandle process_handle); Result OpenHomeMenuProxy(Out> out_system_applet_proxy, - ClientProcessId pid, - InCopyHandle process_handle); + ClientProcessId pid, InCopyHandle process_handle); Result OpenLibraryAppletProxy(Out> out_library_applet_proxy, ClientProcessId pid, InCopyHandle process_handle, - InLargeData attribute); + AppletAttribute attribute); Result OpenLibraryAppletProxyOld( Out> out_library_applet_proxy, ClientProcessId pid, InCopyHandle process_handle); Result OpenOverlayAppletProxy(Out> out_overlay_applet_proxy, ClientProcessId pid, InCopyHandle process_handle, - InLargeData attribute); - Result OpenSystemApplicationProxy(Out> out_system_application_proxy, - ClientProcessId pid, - InCopyHandle process_handle); - Result CreateSelfLibraryAppletCreatorForDevelop(Out> out_library_applet_creator, - ClientProcessId pid, - InCopyHandle process_handle); + AppletAttribute attribute); + Result OpenSystemApplicationProxy( + Out> out_system_application_proxy, + ClientProcessId pid, InCopyHandle process_handle); + Result CreateSelfLibraryAppletCreatorForDevelop( + Out> out_library_applet_creator, ClientProcessId pid, + InCopyHandle process_handle); Result GetSystemAppletControllerForDebug(); Result GetSystemProcessCommonFunctions( Out> out_system_process_common_functions); - Result Cmd460(); + Result GetAppletAlternativeFunctions( + Out> out_applet_alternative_functions); Result GetDebugFunctions(Out> out_debug_functions); private: diff --git a/src/core/hle/service/am/service/applet_alternative_functions.cpp b/src/core/hle/service/am/service/applet_alternative_functions.cpp new file mode 100644 index 000000000..ff1112e17 --- /dev/null +++ b/src/core/hle/service/am/service/applet_alternative_functions.cpp @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/am/service/applet_alternative_functions.h" +#include "core/hle/service/am/window_system.h" +#include "core/hle/service/cmif_serialization.h" + +namespace Service::AM { + +IAppletAlternativeFunctions::IAppletAlternativeFunctions(Core::System& system_, + WindowSystem& window_system) + : ServiceFramework{system_, "IAppletAlternativeFunctions"}, m_window_system{window_system} { + // clang-format off + static const FunctionInfo functions[] = { + {10, D<&IAppletAlternativeFunctions::GetAlternativeTarget>, "GetAlternativeTarget"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IAppletAlternativeFunctions::~IAppletAlternativeFunctions() = default; + +Result IAppletAlternativeFunctions::GetAlternativeTarget(Out out_target) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + *out_target = 0; // Return 0? Or some applet ID? + R_SUCCEED(); +} + +} // namespace Service::AM diff --git a/src/core/hle/service/am/service/applet_alternative_functions.h b/src/core/hle/service/am/service/applet_alternative_functions.h new file mode 100644 index 000000000..2fb208b45 --- /dev/null +++ b/src/core/hle/service/am/service/applet_alternative_functions.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" + +namespace Service::AM { +class WindowSystem; + +class IAppletAlternativeFunctions final : public ServiceFramework { +public: + explicit IAppletAlternativeFunctions(Core::System& system_, WindowSystem& window_system); + ~IAppletAlternativeFunctions() override; + +private: + Result GetAlternativeTarget(Out out_target); + + WindowSystem& m_window_system; +}; + +} // namespace Service::AM diff --git a/src/core/hle/service/am/service/applet_common_functions.cpp b/src/core/hle/service/am/service/applet_common_functions.cpp index cabc68667..7f4c04e20 100644 --- a/src/core/hle/service/am/service/applet_common_functions.cpp +++ b/src/core/hle/service/am/service/applet_common_functions.cpp @@ -20,7 +20,7 @@ IAppletCommonFunctions::IAppletCommonFunctions(Core::System& system_, {20, nullptr, "PushToAppletBoundChannel"}, {21, nullptr, "TryPopFromAppletBoundChannel"}, {40, nullptr, "GetDisplayLogicalResolution"}, - {42, nullptr, "SetDisplayMagnification"}, + {42, D<&IAppletCommonFunctions::SetDisplayMagnification>, "SetDisplayMagnification"}, {50, D<&IAppletCommonFunctions::SetHomeButtonDoubleClickEnabled>, "SetHomeButtonDoubleClickEnabled"}, {51, D<&IAppletCommonFunctions::GetHomeButtonDoubleClickEnabled>, "GetHomeButtonDoubleClickEnabled"}, {52, nullptr, "IsHomeButtonShortPressedBlocked"}, @@ -70,6 +70,13 @@ Result IAppletCommonFunctions::GetHomeButtonDoubleClickEnabled( R_SUCCEED(); } +Result IAppletCommonFunctions::SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height) { + LOG_DEBUG(Service_AM, "(STUBBED) called, x={}, y={}, width={}, height={}", x, y, width, height); + std::scoped_lock lk{applet->lock}; + applet->display_magnification = Common::Rectangle{x, y, x + width, y + height}; + R_SUCCEED(); +} + Result IAppletCommonFunctions::SetCpuBoostRequestPriority(s32 priority) { LOG_WARNING(Service_AM, "(STUBBED) called"); std::scoped_lock lk{applet->lock}; diff --git a/src/core/hle/service/am/service/applet_common_functions.h b/src/core/hle/service/am/service/applet_common_functions.h index a42e57f8c..dc6ec6581 100644 --- a/src/core/hle/service/am/service/applet_common_functions.h +++ b/src/core/hle/service/am/service/applet_common_functions.h @@ -18,6 +18,7 @@ public: private: Result SetHomeButtonDoubleClickEnabled(bool home_button_double_click_enabled); Result GetHomeButtonDoubleClickEnabled(Out out_home_button_double_click_enabled); + Result SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height); Result SetCpuBoostRequestPriority(s32 priority); Result GetCurrentApplicationId(Out out_application_id); Result IsSystemAppletHomeMenu(Out out_is_home_menu); diff --git a/src/core/hle/service/am/service/common_state_getter.cpp b/src/core/hle/service/am/service/common_state_getter.cpp index f48692bad..e4301baf9 100644 --- a/src/core/hle/service/am/service/common_state_getter.cpp +++ b/src/core/hle/service/am/service/common_state_getter.cpp @@ -39,7 +39,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, std::shared_ptr, "GetReaderLockAccessorEx"}, {32, D<&ICommonStateGetter::GetWriterLockAccessorEx>, "GetWriterLockAccessorEx"}, - {40, nullptr, "GetCradleFwVersion"}, + {40, D<&ICommonStateGetter::GetCradleFwVersion>, "GetCradleFwVersion"}, {50, D<&ICommonStateGetter::IsVrModeEnabled>, "IsVrModeEnabled"}, {51, D<&ICommonStateGetter::SetVrModeEnabled>, "SetVrModeEnabled"}, {52, D<&ICommonStateGetter::SetLcdBacklighOffEnabled>, "SetLcdBacklighOffEnabled"}, @@ -265,6 +265,17 @@ Result ICommonStateGetter::GetOperationModeSystemInfo(Out out_operation_mod R_SUCCEED(); } +Result ICommonStateGetter::GetCradleFwVersion(Out out_major, Out out_minor, + Out out_micro) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + *out_major = 0; + *out_minor = 0; + *out_micro = 0; + + R_SUCCEED(); +} + Result ICommonStateGetter::GetAppletLaunchedHistory( Out out_count, OutArray out_applet_ids) { LOG_INFO(Service_AM, "called"); diff --git a/src/core/hle/service/am/service/common_state_getter.h b/src/core/hle/service/am/service/common_state_getter.h index 255884c3a..2418a8263 100644 --- a/src/core/hle/service/am/service/common_state_getter.h +++ b/src/core/hle/service/am/service/common_state_getter.h @@ -37,6 +37,7 @@ private: Result GetDefaultDisplayResolutionChangeEvent(OutCopyHandle out_event); Result GetOperationMode(Out out_operation_mode); Result GetPerformanceMode(Out out_performance_mode); + Result GetCradleFwVersion(Out out_major, Out out_minor, Out out_micro); Result GetBootMode(Out out_boot_mode); Result IsVrModeEnabled(Out out_is_vr_mode_enabled); Result SetVrModeEnabled(bool is_vr_mode_enabled); diff --git a/src/core/hle/service/am/service/library_applet_creator.cpp b/src/core/hle/service/am/service/library_applet_creator.cpp index aa66ebb4a..89f03c148 100644 --- a/src/core/hle/service/am/service/library_applet_creator.cpp +++ b/src/core/hle/service/am/service/library_applet_creator.cpp @@ -106,15 +106,11 @@ std::shared_ptr CreateGuestApplet(Core::System& system, return {}; } - // TODO: enable other versions of applets - enum : u8 { - Firmware1400 = 14, - Firmware1500 = 15, - Firmware1600 = 16, - Firmware1700 = 17, - }; - - auto process = CreateProcess(system, program_id, Firmware1400, Firmware1700); + // We allow any firmware generation to be loaded as a guest process if possible. + // If the emulator lacks the necessary keys, it will fail later and fallback to a stub. + const u8 min_gen = 1; + const u8 max_gen = 255; + auto process = CreateProcess(system, program_id, min_gen, max_gen); if (!process) { // Couldn't initialize the guest process return {}; @@ -166,8 +162,8 @@ std::shared_ptr CreateFrontendApplet(Core::System& syste ILibraryAppletCreator::ILibraryAppletCreator(Core::System& system_, std::shared_ptr applet, WindowSystem& window_system) - : ServiceFramework{system_, "ILibraryAppletCreator"}, - m_window_system{window_system}, m_applet{std::move(applet)} { + : ServiceFramework{system_, "ILibraryAppletCreator"}, m_window_system{window_system}, + m_applet{std::move(applet)} { static const FunctionInfo functions[] = { {0, D<&ILibraryAppletCreator::CreateLibraryApplet>, "CreateLibraryApplet"}, {1, nullptr, "TerminateAllLibraryApplets"}, diff --git a/src/core/hle/service/am/service/overlay_applet_proxy.cpp b/src/core/hle/service/am/service/overlay_applet_proxy.cpp index 8365fd1f6..bcfa975ef 100644 --- a/src/core/hle/service/am/service/overlay_applet_proxy.cpp +++ b/src/core/hle/service/am/service/overlay_applet_proxy.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "core/hle/service/am/service/applet_common_functions.h" #include "core/hle/service/am/service/audio_controller.h" #include "core/hle/service/am/service/common_state_getter.h" #include "core/hle/service/am/service/debug_functions.h" @@ -9,55 +10,33 @@ #include "core/hle/service/am/service/home_menu_functions.h" #include "core/hle/service/am/service/library_applet_creator.h" #include "core/hle/service/am/service/overlay_applet_proxy.h" +#include "core/hle/service/am/service/overlay_functions.h" #include "core/hle/service/am/service/process_winding_controller.h" #include "core/hle/service/am/service/self_controller.h" #include "core/hle/service/am/service/window_controller.h" #include "core/hle/service/cmif_serialization.h" -namespace Service::AM { -// Forward declaration for IOverlayAppletFunctions -class IOverlayAppletFunctions final : public ServiceFramework { -public: - explicit IOverlayAppletFunctions(Core::System& system_) : ServiceFramework{system_, "IOverlayAppletFunctions"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "BeginToWatchShortHomeButtonMessage"}, - {1, nullptr, "EndToWatchShortHomeButtonMessage"}, - {2, nullptr, "GetApplicationIdForLogo"}, - {3, nullptr, "SetGpuTimeSliceBoost"}, - {4, nullptr, "SetAutoSleepTimeAndDimmingTimeEnabled"}, - {5, nullptr, "SetHandlingHomeButtonShortPressedEnabled"}, - {6, nullptr, "SetHandlingCaptureButtonShortPressedEnabledForOverlayApplet"}, - {7, nullptr, "SetHandlingCaptureButtonLongPressedEnabledForOverlayApplet"}, - {8, nullptr, "GetShortHomeButtonMessage"}, - {9, nullptr, "IsHomeButtonShortPressedBlocked"}, - {10, nullptr, "IsVrModeCurtainRequired"}, - {11, nullptr, "SetInputDetectionPolicy"}, - {20, nullptr, "SetCpuBoostRequestPriority"}, - }; - // clang-format on - RegisterHandlers(functions); - } -}; +namespace Service::AM { IOverlayAppletProxy::IOverlayAppletProxy(Core::System& system_, std::shared_ptr applet, Kernel::KProcess* process, WindowSystem& window_system) - : ServiceFramework{system_, "IOverlayAppletProxy"}, - m_window_system{window_system}, m_process{process}, m_applet{std::move(applet)} { + : ServiceFramework{system_, "IOverlayAppletProxy"}, m_window_system{window_system}, + m_process{process}, m_applet{std::move(applet)} { // clang-format off static const FunctionInfo functions[] = { - {0, D<&IOverlayAppletProxy::GetCommonStateGetter>, "GetCommonStateGetter"}, - {1, D<&IOverlayAppletProxy::GetSelfController>, "GetSelfController"}, - {2, D<&IOverlayAppletProxy::GetWindowController>, "GetWindowController"}, - {3, D<&IOverlayAppletProxy::GetAudioController>, "GetAudioController"}, - {4, D<&IOverlayAppletProxy::GetDisplayController>, "GetDisplayController"}, - {11, D<&IOverlayAppletProxy::GetLibraryAppletCreator>, "GetLibraryAppletCreator"}, - {20, D<&IOverlayAppletProxy::GetOverlayAppletFunctions>, "GetOverlayAppletFunctions"}, - {21, D<&IOverlayAppletProxy::GetHomeMenuFunctions>, "GetHomeMenuFunctions"}, - {22, D<&IOverlayAppletProxy::GetGlobalStateController>, "GetGlobalStateController"}, - {1000, D<&IOverlayAppletProxy::GetDebugFunctions>, "GetDebugFunctions"}, - }; + {0, D<&IOverlayAppletProxy::GetCommonStateGetter>, "GetCommonStateGetter"}, + {1, D<&IOverlayAppletProxy::GetSelfController>, "GetSelfController"}, + {2, D<&IOverlayAppletProxy::GetWindowController>, "GetWindowController"}, + {3, D<&IOverlayAppletProxy::GetAudioController>, "GetAudioController"}, + {4, D<&IOverlayAppletProxy::GetDisplayController>, "GetDisplayController"}, + {10, D<&IOverlayAppletProxy::GetProcessWindingController>, "GetProcessWindingController"}, + {11, D<&IOverlayAppletProxy::GetLibraryAppletCreator>, "GetLibraryAppletCreator"}, + {20, D<&IOverlayAppletProxy::GetOverlayFunctions>, "GetOverlayFunctions"}, + {21, D<&IOverlayAppletProxy::GetAppletCommonFunctions>, "GetAppletCommonFunctions"}, + {23, D<&IOverlayAppletProxy::GetGlobalStateController>, "GetGlobalStateController"}, + {1000, D<&IOverlayAppletProxy::GetDebugFunctions>, "GetDebugFunctions"}, +}; // clang-format on RegisterHandlers(functions); @@ -100,13 +79,6 @@ Result IOverlayAppletProxy::GetDisplayController( R_SUCCEED(); } -Result IOverlayAppletProxy::GetProcessWindingController( - Out> out_process_winding_controller) { - LOG_DEBUG(Service_AM, "called"); - *out_process_winding_controller = std::make_shared(system, m_applet); - R_SUCCEED(); -} - Result IOverlayAppletProxy::GetLibraryAppletCreator( Out> out_library_applet_creator) { LOG_DEBUG(Service_AM, "called"); @@ -115,21 +87,6 @@ Result IOverlayAppletProxy::GetLibraryAppletCreator( R_SUCCEED(); } -Result IOverlayAppletProxy::GetOverlayAppletFunctions( - Out> out_overlay_applet_functions) { - LOG_DEBUG(Service_AM, "called"); - *out_overlay_applet_functions = std::make_shared(system); - R_SUCCEED(); -} - -Result IOverlayAppletProxy::GetHomeMenuFunctions( - Out> out_home_menu_functions) { - LOG_DEBUG(Service_AM, "called"); - *out_home_menu_functions = - std::make_shared(system, m_applet, m_window_system); - R_SUCCEED(); -} - Result IOverlayAppletProxy::GetGlobalStateController( Out> out_global_state_controller) { LOG_DEBUG(Service_AM, "called"); @@ -144,4 +101,25 @@ Result IOverlayAppletProxy::GetDebugFunctions( R_SUCCEED(); } -} // namespace Service::AM \ No newline at end of file +Result IOverlayAppletProxy::GetProcessWindingController( + Out> out_process_winding_controller) { + LOG_DEBUG(Service_AM, "called"); + *out_process_winding_controller = std::make_shared(system, m_applet); + R_SUCCEED(); +} + +Result IOverlayAppletProxy::GetOverlayFunctions( + Out> out_overlay_functions) { + LOG_DEBUG(Service_AM, "called"); + *out_overlay_functions = std::make_shared(system, m_applet); + R_SUCCEED(); +} + +Result IOverlayAppletProxy::GetAppletCommonFunctions( + Out> out_applet_common_functions) { + LOG_DEBUG(Service_AM, "called"); + *out_applet_common_functions = std::make_shared(system, m_applet); + R_SUCCEED(); +} + +} // namespace Service::AM diff --git a/src/core/hle/service/am/service/overlay_applet_proxy.h b/src/core/hle/service/am/service/overlay_applet_proxy.h index 9c96993dc..3c0d27ceb 100644 --- a/src/core/hle/service/am/service/overlay_applet_proxy.h +++ b/src/core/hle/service/am/service/overlay_applet_proxy.h @@ -5,6 +5,8 @@ #include "core/hle/service/cmif_types.h" #include "core/hle/service/service.h" +#include "core/hle/service/am/service/applet_common_functions.h" +#include "core/hle/service/am/service/overlay_functions.h" namespace Kernel { class KProcess; @@ -25,6 +27,8 @@ class IProcessWindingController; class ISelfController; class IWindowController; class WindowSystem; +class IOverlayFunctions; +class IAppletCommonFunctions; class IOverlayAppletProxy final : public ServiceFramework { public: @@ -38,11 +42,15 @@ private: Result GetWindowController(Out> out_window_controller); Result GetAudioController(Out> out_audio_controller); Result GetDisplayController(Out> out_display_controller); - Result GetProcessWindingController(Out> out_process_winding_controller); - Result GetLibraryAppletCreator(Out> out_library_applet_creator); - Result GetOverlayAppletFunctions(Out> out_overlay_applet_functions); - Result GetHomeMenuFunctions(Out> out_home_menu_functions); - Result GetGlobalStateController(Out> out_global_state_controller); + Result GetProcessWindingController( + Out> out_process_winding_controller); + Result GetLibraryAppletCreator( + Out> out_library_applet_creator); + Result GetOverlayFunctions(Out> out_overlay_functions); + Result GetAppletCommonFunctions( + Out> out_applet_common_functions); + Result GetGlobalStateController( + Out> out_global_state_controller); Result GetDebugFunctions(Out> out_debug_functions); WindowSystem& m_window_system; @@ -50,4 +58,4 @@ private: const std::shared_ptr m_applet; }; -} // namespace Service::AM \ No newline at end of file +} // namespace Service::AM diff --git a/src/core/hle/service/am/service/overlay_functions.cpp b/src/core/hle/service/am/service/overlay_functions.cpp new file mode 100644 index 000000000..886734e16 --- /dev/null +++ b/src/core/hle/service/am/service/overlay_functions.cpp @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/am/applet.h" +#include "core/hle/service/am/applet_manager.h" +#include "core/hle/service/am/service/overlay_functions.h" +#include "core/hle/service/am/window_system.h" +#include "core/hle/service/cmif_serialization.h" + +namespace Service::AM { +IOverlayFunctions::IOverlayFunctions(Core::System& system_, std::shared_ptr applet) + : ServiceFramework{system_, "IOverlayFunctions"}, m_applet{std::move(applet)} { + // clang-format off + static const FunctionInfo functions[] = { + {0, D<&IOverlayFunctions::BeginToWatchShortHomeButtonMessage>, "BeginToWatchShortHomeButtonMessage"}, + {1, D<&IOverlayFunctions::EndToWatchShortHomeButtonMessage>, "EndToWatchShortHomeButtonMessage"}, + {2, D<&IOverlayFunctions::GetApplicationIdForLogo>, "GetApplicationIdForLogo"}, + {3, nullptr, "SetGpuTimeSliceBoost"}, + {4, D<&IOverlayFunctions::SetAutoSleepTimeAndDimmingTimeEnabled>, "SetAutoSleepTimeAndDimmingTimeEnabled"}, + {5, nullptr, "TerminateApplicationAndSetReason"}, + {6, nullptr, "SetScreenShotPermissionGlobally"}, + {10, nullptr, "StartShutdownSequenceForOverlay"}, + {11, nullptr, "StartRebootSequenceForOverlay"}, + {20, D<&IOverlayFunctions::SetHandlingHomeButtonShortPressedEnabled>, "SetHandlingHomeButtonShortPressedEnabled"}, + {21, nullptr, "SetHandlingTouchScreenInputEnabled"}, + {30, nullptr, "SetHealthWarningShowingState"}, + {31, D<&IOverlayFunctions::IsHealthWarningRequired>, "IsHealthWarningRequired"}, + {40, nullptr, "GetApplicationNintendoLogo"}, + {41, nullptr, "GetApplicationStartupMovie"}, + {50, nullptr, "SetGpuTimeSliceBoostForApplication"}, + {60, nullptr, "Unknown60"}, + {70, D<&IOverlayFunctions::Unknown70>, "Unknown70"}, + {90, nullptr, "SetRequiresGpuResourceUse"}, + {101, nullptr, "BeginToObserveHidInputForDevelop"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IOverlayFunctions::~IOverlayFunctions() = default; + +Result IOverlayFunctions::BeginToWatchShortHomeButtonMessage() { + LOG_DEBUG(Service_AM, "called"); + + m_applet->overlay_in_foreground = true; + m_applet->home_button_short_pressed_blocked = false; + + if (auto* window_system = system.GetAppletManager().GetWindowSystem()) { + window_system->RequestUpdate(); + } + + R_SUCCEED(); +} + +Result IOverlayFunctions::EndToWatchShortHomeButtonMessage() { + LOG_DEBUG(Service_AM, "called"); + + m_applet->overlay_in_foreground = false; + m_applet->home_button_short_pressed_blocked = false; + + if (auto* window_system = system.GetAppletManager().GetWindowSystem()) { + window_system->RequestUpdate(); + } + + R_SUCCEED(); +} + +Result IOverlayFunctions::GetApplicationIdForLogo(Out out_application_id) { + LOG_DEBUG(Service_AM, "called"); + + std::shared_ptr target_applet; + + auto* window_system = system.GetAppletManager().GetWindowSystem(); + if (window_system) { + target_applet = window_system->GetMainApplet(); + if (target_applet) { + std::scoped_lock lk{target_applet->lock}; + LOG_DEBUG(Service_AM, "applet_id={}, program_id={:016X}, type={}", + static_cast(target_applet->applet_id), target_applet->program_id, + static_cast(target_applet->type)); + + u64 id = target_applet->screen_shot_identity.application_id; + if (id == 0) { + id = target_applet->program_id; + } + LOG_DEBUG(Service_AM, "application_id={:016X}", id); + *out_application_id = id; + R_SUCCEED(); + } + } + + std::scoped_lock lk{m_applet->lock}; + u64 id = m_applet->screen_shot_identity.application_id; + if (id == 0) { + id = m_applet->program_id; + } + LOG_DEBUG(Service_AM, "application_id={:016X} (fallback)", id); + *out_application_id = id; + R_SUCCEED(); +} + +Result IOverlayFunctions::SetAutoSleepTimeAndDimmingTimeEnabled(bool enabled) { + LOG_WARNING(Service_AM, "(STUBBED) called, enabled={}", enabled); + std::scoped_lock lk{m_applet->lock}; + m_applet->auto_sleep_disabled = !enabled; + R_SUCCEED(); +} + +Result IOverlayFunctions::IsHealthWarningRequired(Out is_required) { + LOG_DEBUG(Service_AM, "called"); + std::scoped_lock lk{m_applet->lock}; + *is_required = false; + R_SUCCEED(); +} + +Result IOverlayFunctions::SetHandlingHomeButtonShortPressedEnabled(bool enabled) { + LOG_DEBUG(Service_AM, "called, enabled={}", enabled); + std::scoped_lock lk{m_applet->lock}; + m_applet->home_button_short_pressed_blocked = !enabled; + R_SUCCEED(); +} + +Result IOverlayFunctions::Unknown70() { + LOG_DEBUG(Service_AM, "called"); + R_SUCCEED(); +} +} // namespace Service::AM diff --git a/src/core/hle/service/am/service/overlay_functions.h b/src/core/hle/service/am/service/overlay_functions.h new file mode 100644 index 000000000..cef507dd7 --- /dev/null +++ b/src/core/hle/service/am/service/overlay_functions.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::AM { + +struct Applet; + +class IOverlayFunctions final : public ServiceFramework { +public: + explicit IOverlayFunctions(Core::System& system_, std::shared_ptr applet_); + ~IOverlayFunctions() override; + +private: + Result BeginToWatchShortHomeButtonMessage(); + Result EndToWatchShortHomeButtonMessage(); + Result GetApplicationIdForLogo(Out out_application_id); + Result SetAutoSleepTimeAndDimmingTimeEnabled(bool enabled); + Result IsHealthWarningRequired(Out is_required); + Result SetHandlingHomeButtonShortPressedEnabled(bool enabled); + Result Unknown70(); + +private: + const std::shared_ptr m_applet; +}; + +} // namespace Service::AM diff --git a/src/core/hle/service/am/window_system.cpp b/src/core/hle/service/am/window_system.cpp index f4e4e2715..b340a0afa 100644 --- a/src/core/hle/service/am/window_system.cpp +++ b/src/core/hle/service/am/window_system.cpp @@ -21,6 +21,12 @@ void WindowSystem::SetEventObserver(EventObserver* observer) { m_system.GetAppletManager().SetWindowSystem(this); } +void WindowSystem::RequestUpdate() { + if (m_event_observer) { + m_event_observer->RequestUpdate(); + } +} + void WindowSystem::Update() { std::scoped_lock lk{m_lock}; @@ -40,7 +46,10 @@ void WindowSystem::Update() { void WindowSystem::TrackApplet(std::shared_ptr applet, bool is_application) { std::scoped_lock lk{m_lock}; - if (applet->applet_id == AppletId::QLaunch) { + if (applet->type == AppletType::OverlayApplet) { + ASSERT(overlay_display_applet == nullptr); + overlay_display_applet = applet; + } else if (applet->applet_id == AppletId::QLaunch) { ASSERT(m_home_menu == nullptr); m_home_menu = applet.get(); } else if (is_application) { @@ -320,4 +329,8 @@ void WindowSystem::SetHomeMenuRequestCallback(HomeMenuRequestCallback callback) m_home_menu_request_callback = std::move(callback); } +std::shared_ptr WindowSystem::GetOverlayDisplayApplet() { + return overlay_display_applet; +} + } // namespace Service::AM diff --git a/src/core/hle/service/am/window_system.h b/src/core/hle/service/am/window_system.h index 4e1bf4cb4..8205f06f7 100644 --- a/src/core/hle/service/am/window_system.h +++ b/src/core/hle/service/am/window_system.h @@ -35,11 +35,13 @@ public: public: void SetEventObserver(EventObserver* event_observer); void Update(); + void RequestUpdate(); public: void TrackApplet(std::shared_ptr applet, bool is_application); std::shared_ptr GetByAppletResourceUserId(u64 aruid); std::shared_ptr GetMainApplet(); + std::shared_ptr GetOverlayDisplayApplet(); public: void RequestHomeMenuToGetForeground(); @@ -74,6 +76,9 @@ private: // Lock. std::mutex m_lock{}; + // Overlay Display Applet. + std::shared_ptr overlay_display_applet; + // Home menu state. bool m_home_menu_foreground_locked{}; Applet* m_foreground_requested_applet{}; diff --git a/src/core/hle/service/aoc/addon_content_manager.cpp b/src/core/hle/service/aoc/addon_content_manager.cpp index fcc3525c3..a5d61464b 100644 --- a/src/core/hle/service/aoc/addon_content_manager.cpp +++ b/src/core/hle/service/aoc/addon_content_manager.cpp @@ -45,6 +45,11 @@ static std::vector AccumulateAOCTitleIDs(Core::System& system) { Loader::ResultStatus::Success; }), add_on_content.end()); + + LOG_WARNING(Service_AOC, "Accumulated {} AOC title IDs", add_on_content.size()); + for (const auto& tid : add_on_content) { + LOG_WARNING(Service_AOC, "Found AOC: {:016X}", tid); + } return add_on_content; } @@ -53,8 +58,8 @@ IAddOnContentManager::IAddOnContentManager(Core::System& system_) service_context{system_, "aoc:u"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "CountAddOnContentByApplicationId"}, - {1, nullptr, "ListAddOnContentByApplicationId"}, + {0, D<&IAddOnContentManager::CountAddOnContentByApplicationId>, "CountAddOnContentByApplicationId"}, + {1, D<&IAddOnContentManager::ListAddOnContentByApplicationId>, "ListAddOnContentByApplicationId"}, {2, D<&IAddOnContentManager::CountAddOnContent>, "CountAddOnContent"}, {3, D<&IAddOnContentManager::ListAddOnContent>, "ListAddOnContent"}, {4, nullptr, "GetAddOnContentBaseIdByApplicationId"}, @@ -108,21 +113,85 @@ Result IAddOnContentManager::CountAddOnContent(Out out_count, ClientProcess Result IAddOnContentManager::ListAddOnContent(Out out_count, OutBuffer out_addons, u32 offset, u32 count, ClientProcessId process_id) { - LOG_DEBUG(Service_AOC, "called with offset={}, count={}, process_id={}", offset, count, - process_id.pid); + LOG_WARNING(Service_AOC, "called with offset={}, count={}, process_id={}", offset, count, + process_id.pid); const auto current = FileSys::GetBaseTitleID(system.GetApplicationProcessProgramID()); std::vector out; const auto& disabled = Settings::values.disabled_addons[current]; if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) { + LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current); for (u64 content_id : add_on_content) { - if (FileSys::GetBaseTitleID(content_id) != current) { + const auto aoc_base = FileSys::GetBaseTitleID(content_id); + if (aoc_base != current) { + // LOG_WARNING(Service_AOC, "Skipping AOC {:016X} (Base: {:016X})", content_id, + // aoc_base); continue; } + LOG_WARNING(Service_AOC, "Match! AOC {:016X} belongs to current app. Adding to list.", + content_id); out.push_back(static_cast(FileSys::GetAOCID(content_id))); } + } else { + LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current); + } + + // TODO(DarkLordZach): Find the correct error code. + R_UNLESS(out.size() >= offset, ResultUnknown); + + *out_count = static_cast(std::min(out.size() - offset, count)); + std::rotate(out.begin(), out.begin() + offset, out.end()); + + std::memcpy(out_addons.data(), out.data(), *out_count * sizeof(u32)); + + R_SUCCEED(); +} + +Result IAddOnContentManager::CountAddOnContentByApplicationId(Out out_count, + u64 application_id) { + LOG_WARNING(Service_AOC, "called. application_id={:016X}", application_id); + + const auto current = application_id; + + const auto& disabled = Settings::values.disabled_addons[current]; + if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end()) { + *out_count = 0; + R_SUCCEED(); + } + + *out_count = static_cast( + std::count_if(add_on_content.begin(), add_on_content.end(), + [current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); })); + + R_SUCCEED(); +} + +Result IAddOnContentManager::ListAddOnContentByApplicationId( + Out out_count, OutBuffer out_addons, u32 offset, u32 count, + u64 application_id) { + LOG_WARNING(Service_AOC, "called with offset={}, count={}, application_id={:016X}", offset, + count, application_id); + + const auto current = FileSys::GetBaseTitleID(application_id); + + std::vector out; + const auto& disabled = Settings::values.disabled_addons[current]; + if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) { + LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current); + for (u64 content_id : add_on_content) { + const auto aoc_base = FileSys::GetBaseTitleID(content_id); + if (aoc_base != current) { + continue; + } + + LOG_WARNING(Service_AOC, "Match! AOC {:016X} belongs to current app. Adding to list.", + content_id); + out.push_back(static_cast(FileSys::GetAOCID(content_id))); + } + } else { + LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current); } // TODO(DarkLordZach): Find the correct error code. diff --git a/src/core/hle/service/aoc/addon_content_manager.h b/src/core/hle/service/aoc/addon_content_manager.h index c56a462ea..d30393827 100644 --- a/src/core/hle/service/aoc/addon_content_manager.h +++ b/src/core/hle/service/aoc/addon_content_manager.h @@ -29,6 +29,10 @@ public: Result CountAddOnContent(Out out_count, ClientProcessId process_id); Result ListAddOnContent(Out out_count, OutBuffer out_addons, u32 offset, u32 count, ClientProcessId process_id); + Result CountAddOnContentByApplicationId(Out out_count, u64 application_id); + Result ListAddOnContentByApplicationId(Out out_count, + OutBuffer out_addons, + u32 offset, u32 count, u64 application_id); Result GetAddOnContentBaseId(Out out_title_id, ClientProcessId process_id); Result PrepareAddOnContent(s32 addon_index, ClientProcessId process_id); Result GetAddOnContentListChangedEvent(OutCopyHandle out_event); diff --git a/src/core/hle/service/caps/caps_a.cpp b/src/core/hle/service/caps/caps_a.cpp index 52228b830..8ebec832a 100644 --- a/src/core/hle/service/caps/caps_a.cpp +++ b/src/core/hle/service/caps/caps_a.cpp @@ -53,6 +53,7 @@ IAlbumAccessorService::IAlbumAccessorService(Core::System& system_, {8021, nullptr, "GetAlbumEntryFromApplicationAlbumEntryAruid"}, {10011, nullptr, "SetInternalErrorConversionEnabled"}, {50000, nullptr, "LoadMakerNoteInfoForDebug"}, + {50011, C<&IAlbumAccessorService::SetShimLibraryVersion>, "SetShimLibraryVersion"}, {60002, nullptr, "OpenAccessorSession"}, }; // clang-format on @@ -137,6 +138,14 @@ Result IAlbumAccessorService::LoadAlbumScreenShotThumbnailImageEx1( R_RETURN(TranslateResult(result)); } +Result IAlbumAccessorService::SetShimLibraryVersion(u64 shim_library_version, + u64 applet_resource_user_id) { + LOG_WARNING(Service_Capture, + "(STUBBED) called, shim_library_version={}, applet_resource_user_id={}", + shim_library_version, applet_resource_user_id); + R_SUCCEED(); +} + Result IAlbumAccessorService::TranslateResult(Result in_result) { if (in_result.IsSuccess()) { return in_result; diff --git a/src/core/hle/service/caps/caps_a.h b/src/core/hle/service/caps/caps_a.h index c7a5208e3..d5833c194 100644 --- a/src/core/hle/service/caps/caps_a.h +++ b/src/core/hle/service/caps/caps_a.h @@ -50,6 +50,8 @@ private: OutArray out_image, OutArray out_buffer); + Result SetShimLibraryVersion(u64 shim_library_version, u64 applet_resource_user_id); + Result TranslateResult(Result in_result); std::shared_ptr manager = nullptr; diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index b34ba658a..ac69a10c7 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -72,7 +72,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) {24, nullptr, "RegisterSaveDataFileSystemAtomicDeletion"}, {25, nullptr, "DeleteSaveDataFileSystemBySaveDataSpaceId"}, {26, nullptr, "FormatSdCardDryRun"}, - {27, nullptr, "IsExFatSupported"}, + {27, D<&FSP_SRV::IsExFatSupported>, "IsExFatSupported"}, {28, nullptr, "DeleteSaveDataFileSystemBySaveDataAttribute"}, {30, D<&FSP_SRV::OpenGameCardStorage>, "OpenGameCardStorage"}, {31, D<&FSP_SRV::OpenGameCardFileSystem>, "OpenGameCardFileSystem"}, @@ -81,6 +81,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) {34, D<&FSP_SRV::GetCacheStorageSize>, "GetCacheStorageSize"}, {35, nullptr, "CreateSaveDataFileSystemByHashSalt"}, {36, nullptr, "OpenHostFileSystemWithOption"}, + {37, D<&FSP_SRV::OpenSaveDataTransferManager>, "OpenSaveDataTransferManager"}, {51, D<&FSP_SRV::OpenSaveDataFileSystem>, "OpenSaveDataFileSystem"}, {52, D<&FSP_SRV::OpenSaveDataFileSystemBySystemSaveDataId>, "OpenSaveDataFileSystemBySystemSaveDataId"}, {53, D<&FSP_SRV::OpenReadOnlySaveDataFileSystem>, "OpenReadOnlySaveDataFileSystem"}, @@ -252,8 +253,8 @@ Result FSP_SRV::CreateSaveDataFileSystemBySystemSaveDataId( Result FSP_SRV::OpenSaveDataFileSystem(OutInterface out_interface, FileSys::SaveDataSpaceId space_id, FileSys::SaveDataAttribute attribute) { - LOG_INFO(Service_FS, "called, space_id={:02X}, program_id={:016X}", - static_cast(space_id), attribute.program_id); + LOG_INFO(Service_FS, "called, space_id={:02X}, program_id={:016X}", static_cast(space_id), + attribute.program_id); FileSys::VirtualDir dir{}; // This triggers the 'Smart Pull' (Ryujinx -> Citron) in savedata_factory.cpp @@ -278,9 +279,9 @@ Result FSP_SRV::OpenSaveDataFileSystem(OutInterface out_interface, // Wrap the directory in the IFileSystem interface. // We pass 'save_data_controller->GetFactory()' so the Commit function can find the Mirror. - *out_interface = std::make_shared( - system, std::move(dir), SizeGetter::FromStorageId(fsc, id), - save_data_controller->GetFactory(), space_id, attribute); + *out_interface = + std::make_shared(system, std::move(dir), SizeGetter::FromStorageId(fsc, id), + save_data_controller->GetFactory(), space_id, attribute); R_SUCCEED(); } @@ -338,8 +339,7 @@ Result FSP_SRV::WriteSaveDataFileSystemExtraData(InBufferWriteSaveDataExtraData(extra_data, space_id, - extra_data.attr)); + R_RETURN(save_data_controller->WriteSaveDataExtraData(extra_data, space_id, extra_data.attr)); } Result FSP_SRV::WriteSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute( @@ -362,8 +362,8 @@ Result FSP_SRV::WriteSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute( std::memcpy(&extra_data, buffer.data(), sizeof(FileSys::SaveDataExtraData)); std::memcpy(&mask, mask_buffer.data(), sizeof(FileSys::SaveDataExtraData)); - R_RETURN( - save_data_controller->WriteSaveDataExtraDataWithMask(extra_data, mask, space_id, attribute)); + R_RETURN(save_data_controller->WriteSaveDataExtraDataWithMask(extra_data, mask, space_id, + attribute)); } Result FSP_SRV::ReadSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute( @@ -415,7 +415,7 @@ Result FSP_SRV::ReadSaveDataFileSystemExtraData(OutBufferReadSaveDataExtraData(&extra_data, FileSys::SaveDataSpaceId::User, - attribute)); + attribute)); std::memcpy(out_buffer.data(), &extra_data, sizeof(FileSys::SaveDataExtraData)); R_SUCCEED(); @@ -660,7 +660,8 @@ Result FSP_SRV::OpenSaveDataTransferManager(OutInterface out_interface) { +Result FSP_SRV::OpenSaveDataTransferManagerVersion2( + OutInterface out_interface) { LOG_DEBUG(Service_FS, "called"); *out_interface = std::make_shared(system); @@ -690,18 +691,28 @@ Result FSP_SRV::DeleteSaveDataFileSystem(u64 save_data_id) { R_SUCCEED(); } -Result FSP_SRV::OpenGameCardStorage(OutInterface out_interface, u32 handle, u32 partition_id) { +Result FSP_SRV::OpenGameCardStorage(OutInterface out_interface, u32 handle, + u32 partition_id) { LOG_WARNING(Service_FS, "(STUBBED) called, handle={}, partition_id={}", handle, partition_id); // Would need to open game card storage for the specified handle and partition R_THROW(FileSys::ResultTargetNotFound); } -Result FSP_SRV::OpenGameCardFileSystem(OutInterface out_interface, u32 handle, u32 partition_id) { +Result FSP_SRV::OpenGameCardFileSystem(OutInterface out_interface, u32 handle, + u32 partition_id) { LOG_WARNING(Service_FS, "(STUBBED) called, handle={}, partition_id={}", handle, partition_id); // Would need to open game card filesystem for the specified handle and partition R_THROW(FileSys::ResultTargetNotFound); } +Result FSP_SRV::IsExFatSupported(Out out_is_supported) { + LOG_WARNING(Service_FS, "(STUBBED) called"); + + *out_is_supported = true; + + R_SUCCEED(); +} + } // namespace Service::FileSystem diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.h b/src/core/hle/service/filesystem/fsp/fsp_srv.h index f23a2fb22..8f683ac26 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.h @@ -116,12 +116,15 @@ private: Result OpenSdCardDetectionEventNotifier(OutInterface out_interface); Result OpenGameCardDetectionEventNotifier(OutInterface out_interface); Result OpenSaveDataTransferManager(OutInterface out_interface); - Result OpenSaveDataTransferManagerVersion2(OutInterface out_interface); + Result OpenSaveDataTransferManagerVersion2( + OutInterface out_interface); Result OpenBisWiper(OutInterface out_interface); Result OpenBisStorage(OutInterface out_interface, u32 partition_id); Result DeleteSaveDataFileSystem(u64 save_data_id); Result OpenGameCardStorage(OutInterface out_interface, u32 handle, u32 partition_id); - Result OpenGameCardFileSystem(OutInterface out_interface, u32 handle, u32 partition_id); + Result OpenGameCardFileSystem(OutInterface out_interface, u32 handle, + u32 partition_id); + Result IsExFatSupported(Out out_is_supported); FileSystemController& fsc; const FileSys::ContentProvider& content_provider; diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 49dd1a04b..1299f715e 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include "core/core.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/ipc_helpers.h" @@ -172,8 +173,8 @@ constexpr Result ResultNetworkCommunicationDisabled{ErrorModule::NIFM, 1111}; class IScanRequest final : public ServiceFramework { public: - explicit IScanRequest(Core::System& system_) : ServiceFramework{system_, "IScanRequest"}, - service_context{system_, "IScanRequest"} { + explicit IScanRequest(Core::System& system_) + : ServiceFramework{system_, "IScanRequest"}, service_context{system_, "IScanRequest"} { // clang-format off static const FunctionInfo functions[] = { {0, &IScanRequest::Submit, "Submit"}, @@ -604,6 +605,41 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) { + LOG_WARNING(Service_NIFM, "(STUBBED) called"); + + const auto adapters = Network::GetAvailableNetworkInterfaces(); + constexpr size_t kEntrySize = 0x3F0; // From official + + std::vector blob(adapters.size() * kEntrySize, 0); + + for (size_t i = 0; i < adapters.size(); ++i) { + const auto& host = adapters[i]; + u8* const base = blob.data() + i * kEntrySize; + + // Match expected structure + // Citron NetworkInterface doesn't have type/kind, so we use 2u (Ethernet) as a safe default + *reinterpret_cast(base + 0x0) = 2u; + *reinterpret_cast(base + 0x4) = 1u; // Status? + + std::memcpy(base + 0x18, &host.ip_address, sizeof(host.ip_address)); + std::memcpy(base + 0x1C, &host.subnet_mask, sizeof(host.subnet_mask)); + std::memcpy(base + 0x20, &host.gateway, sizeof(host.gateway)); + + std::string name_utf8 = host.name; + name_utf8.resize(0x110, '\0'); + std::memcpy(base + 0x2E0, name_utf8.data(), 0x110); + } + + if (ctx.GetWriteBufferSize() > 0 && !blob.empty()) { + ctx.WriteBuffer(blob.data(), std::min(ctx.GetWriteBufferSize(), blob.size())); + } + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(static_cast(adapters.size())); +} + void IGeneralService::RemoveNetworkProfile(HLERequestContext& ctx) { LOG_WARNING(Service_NIFM, "(STUBBED) called"); @@ -691,6 +727,31 @@ void IGeneralService::GetCurrentIpConfigInfo(HLERequestContext& ctx) { rb.PushRaw(ip_config_info); } +void IGeneralService::EnumerateNetworkProfiles(HLERequestContext& ctx) { + LOG_WARNING(Service_NIFM, "(STUBBED) called"); + + // Return 0 profiles for now (stub) + ctx.WriteBuffer(std::span{}); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(0); // Number of profiles +} + +void IGeneralService::ConfirmSystemAvailability(HLERequestContext& ctx) { + LOG_WARNING(Service_NIFM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + +void IGeneralService::SetBackgroundRequestEnabled(HLERequestContext& ctx) { + LOG_WARNING(Service_NIFM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void IGeneralService::IsWirelessCommunicationEnabled(HLERequestContext& ctx) { LOG_WARNING(Service_NIFM, "(STUBBED) called"); @@ -750,6 +811,16 @@ void IGeneralService::IsAnyForegroundRequestAccepted(HLERequestContext& ctx) { rb.Push(is_accepted); } +void IGeneralService::GetSsidListVersion(HLERequestContext& ctx) { + LOG_WARNING(Service_NIFM, "(STUBBED) called"); + + constexpr u32 ssid_list_version = 0; + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(ssid_list_version); +} + void IGeneralService::GetNetworkProfile(HLERequestContext& ctx) { LOG_WARNING(Service_NIFM, "(STUBBED) called"); @@ -846,7 +917,8 @@ void IGeneralService::IsNetworkEmulationFeatureEnabled(HLERequestContext& ctx) { } void IGeneralService::SelectActiveNetworkEmulationProfileIdForDebug(HLERequestContext& ctx) { - LOG_WARNING(Service_NIFM, "(STUBBED) called SelectActiveNetworkEmulationProfileIdForDebug [18.0.0+]"); + LOG_WARNING(Service_NIFM, + "(STUBBED) called SelectActiveNetworkEmulationProfileIdForDebug [18.0.0+]"); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -898,7 +970,8 @@ void IGeneralService::DestroyRewriteRule(HLERequestContext& ctx) { } void IGeneralService::IsActiveNetworkEmulationProfileIdSelected(HLERequestContext& ctx) { - LOG_WARNING(Service_NIFM, "(STUBBED) called IsActiveNetworkEmulationProfileIdSelected [20.0.0+]"); + LOG_WARNING(Service_NIFM, + "(STUBBED) called IsActiveNetworkEmulationProfileIdSelected [20.0.0+]"); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -935,8 +1008,8 @@ IGeneralService::IGeneralService(Core::System& system_) {2, &IGeneralService::CreateScanRequest, "CreateScanRequest"}, {4, &IGeneralService::CreateRequest, "CreateRequest"}, {5, &IGeneralService::GetCurrentNetworkProfile, "GetCurrentNetworkProfile"}, - {6, nullptr, "EnumerateNetworkInterfaces"}, - {7, nullptr, "EnumerateNetworkProfiles"}, + {6, &IGeneralService::EnumerateNetworkInterfaces, "EnumerateNetworkInterfaces"}, + {7, &IGeneralService::EnumerateNetworkProfiles, "EnumerateNetworkProfiles"}, {8, &IGeneralService::GetNetworkProfile, "GetNetworkProfile"}, {9, &IGeneralService::SetNetworkProfile, "SetNetworkProfile"}, {10, &IGeneralService::RemoveNetworkProfile, "RemoveNetworkProfile"}, @@ -954,7 +1027,7 @@ IGeneralService::IGeneralService(Core::System& system_) {22, &IGeneralService::IsAnyForegroundRequestAccepted, "IsAnyForegroundRequestAccepted"}, {23, nullptr, "PutToSleep"}, {24, nullptr, "WakeUp"}, - {25, nullptr, "GetSsidListVersion"}, + {25, &IGeneralService::GetSsidListVersion, "GetSsidListVersion"}, {26, nullptr, "SetExclusiveClient"}, {27, nullptr, "GetDefaultIpSetting"}, {28, nullptr, "SetDefaultIpSetting"}, @@ -962,8 +1035,8 @@ IGeneralService::IGeneralService(Core::System& system_) {30, nullptr, "SetEthernetCommunicationEnabledForTest"}, {31, nullptr, "GetTelemetorySystemEventReadableHandle"}, {32, nullptr, "GetTelemetryInfo"}, - {33, nullptr, "ConfirmSystemAvailability"}, - {34, nullptr, "SetBackgroundRequestEnabled"}, + {33, &IGeneralService::ConfirmSystemAvailability, "ConfirmSystemAvailability"}, + {34, &IGeneralService::SetBackgroundRequestEnabled, "SetBackgroundRequestEnabled"}, {35, &IGeneralService::GetScanData, "GetScanData"}, {36, &IGeneralService::GetCurrentAccessPoint, "GetCurrentAccessPoint"}, {37, &IGeneralService::Shutdown, "Shutdown"}, diff --git a/src/core/hle/service/nifm/nifm.h b/src/core/hle/service/nifm/nifm.h index 550fba683..fdca95644 100644 --- a/src/core/hle/service/nifm/nifm.h +++ b/src/core/hle/service/nifm/nifm.h @@ -28,6 +28,7 @@ private: void CreateScanRequest(HLERequestContext& ctx); void CreateRequest(HLERequestContext& ctx); void GetCurrentNetworkProfile(HLERequestContext& ctx); + void EnumerateNetworkInterfaces(HLERequestContext& ctx); void RemoveNetworkProfile(HLERequestContext& ctx); void GetCurrentIpAddress(HLERequestContext& ctx); void CreateTemporaryNetworkProfile(HLERequestContext& ctx); @@ -37,6 +38,7 @@ private: void IsEthernetCommunicationEnabled(HLERequestContext& ctx); void IsAnyInternetRequestAccepted(HLERequestContext& ctx); void IsAnyForegroundRequestAccepted(HLERequestContext& ctx); + void GetSsidListVersion(HLERequestContext& ctx); void SetWowlDelayedWakeTime(HLERequestContext& ctx); void GetNetworkProfile(HLERequestContext& ctx); void SetNetworkProfile(HLERequestContext& ctx); @@ -47,6 +49,9 @@ private: void SetAcceptableNetworkTypeFlag(HLERequestContext& ctx); void GetAcceptableNetworkTypeFlag(HLERequestContext& ctx); void NotifyConnectionStateChanged(HLERequestContext& ctx); + void EnumerateNetworkProfiles(HLERequestContext& ctx); + void ConfirmSystemAvailability(HLERequestContext& ctx); + void SetBackgroundRequestEnabled(HLERequestContext& ctx); void SetWowlTcpKeepAliveTimeout(HLERequestContext& ctx); void IsWiredConnectionAvailable(HLERequestContext& ctx); void IsNetworkEmulationFeatureEnabled(HLERequestContext& ctx); diff --git a/src/core/hle/service/npns/npns.cpp b/src/core/hle/service/npns/npns.cpp index cbac17cff..f0aa9494d 100644 --- a/src/core/hle/service/npns/npns.cpp +++ b/src/core/hle/service/npns/npns.cpp @@ -26,7 +26,7 @@ public: {5, C<&INpnsSystem::GetReceiveEvent>, "GetReceiveEvent"}, {6, nullptr, "ListenUndelivered"}, {7, nullptr, "GetStateChangeEvent"}, - {8, nullptr, "ListenToByName"}, + {8, C<&INpnsSystem::ListenToByName>, "ListenToByName"}, {11, nullptr, "SubscribeTopic"}, {12, nullptr, "UnsubscribeTopic"}, {13, nullptr, "QueryIsTopicExist"}, @@ -64,10 +64,10 @@ public: {70, nullptr, "UnknownCmd70"}, {101, nullptr, "Suspend"}, {102, nullptr, "Resume"}, - {103, nullptr, "GetState"}, + {103, C<&INpnsSystem::GetState>, "GetState"}, {104, nullptr, "GetStatistics"}, {105, nullptr, "GetPlayReportRequestEvent"}, - {106, nullptr, "GetLastNotifiedTime"}, + {106, C<&INpnsSystem::GetLastNotifiedTime>, "GetLastNotifiedTime"}, {107, nullptr, "SetLastNotifiedTime"}, {111, nullptr, "GetJid"}, {112, nullptr, "CreateJid"}, @@ -88,7 +88,7 @@ public: {154, nullptr, "CreateTokenAsync"}, {155, nullptr, "CreateTokenAsyncWithApplicationId"}, {156, nullptr, "CreateTokenWithNameAsync"}, - {161, nullptr, "GetRequestChangeStateCancelEvent"}, + {161, C<&INpnsSystem::GetRequestChangeStateCancelEvent>, "GetRequestChangeStateCancelEvent"}, {162, nullptr, "RequestChangeStateForceTimedWithCancelEvent"}, {201, nullptr, "RequestChangeStateForceTimed"}, {202, nullptr, "RequestChangeStateForceAsync"}, @@ -105,10 +105,13 @@ public: RegisterHandlers(functions); get_receive_event = service_context.CreateEvent("npns:s:GetReceiveEvent"); + get_request_change_state_cancel_event = + service_context.CreateEvent("npns:s:GetRequestChangeStateCancelEvent"); } ~INpnsSystem() override { service_context.CloseEvent(get_receive_event); + service_context.CloseEvent(get_request_change_state_cancel_event); } private: @@ -124,29 +127,54 @@ private: R_SUCCEED(); } + Result GetState(Out out_state) { + LOG_INFO(Service_NPNS, "called"); + *out_state = 0; + R_SUCCEED(); + } + + Result GetLastNotifiedTime(Out out_last_notified_time) { + LOG_INFO(Service_NPNS, "called"); + *out_last_notified_time = 0; + R_SUCCEED(); + } + + Result GetRequestChangeStateCancelEvent(OutCopyHandle out_event) { + LOG_INFO(Service_NPNS, "called"); + *out_event = &get_request_change_state_cancel_event->GetReadableEvent(); + R_SUCCEED(); + } + + Result ListenToByName() { + LOG_DEBUG(Service_NPNS, "(STUBBED) called."); + R_SUCCEED(); + } + KernelHelpers::ServiceContext service_context; Kernel::KEvent* get_receive_event; + Kernel::KEvent* get_request_change_state_cancel_event; }; class INpnsUser final : public ServiceFramework { public: - explicit INpnsUser(Core::System& system_) : ServiceFramework{system_, "npns:u"} { + explicit INpnsUser(Core::System& system_) + : ServiceFramework{system_, "npns:u"}, service_context{system, "npns:u"} { // clang-format off static const FunctionInfo functions[] = { {1, nullptr, "ListenAll"}, {2, nullptr, "ListenTo"}, {3, nullptr, "Receive"}, {4, nullptr, "ReceiveRaw"}, - {5, nullptr, "GetReceiveEvent"}, + {5, C<&INpnsUser::GetReceiveEvent>, "GetReceiveEvent"}, {7, nullptr, "GetStateChangeEvent"}, - {8, nullptr, "ListenToByName"}, + {8, C<&INpnsUser::ListenToByName>, "ListenToByName"}, {21, nullptr, "CreateToken"}, {23, nullptr, "DestroyToken"}, {25, nullptr, "QueryIsTokenValid"}, {26, nullptr, "ListenToMyApplicationId"}, {101, nullptr, "Suspend"}, {102, nullptr, "Resume"}, - {103, nullptr, "GetState"}, + {103, C<&INpnsUser::GetState>, "GetState"}, {104, nullptr, "GetStatistics"}, {111, nullptr, "GetJid"}, {120, nullptr, "CreateNotificationReceiver"}, @@ -158,7 +186,37 @@ public: // clang-format on RegisterHandlers(functions); + + get_receive_event = service_context.CreateEvent("npns:u:GetReceiveEvent"); } + + ~INpnsUser() override { + service_context.CloseEvent(get_receive_event); + } + +private: + Result ListenToByName(InBuffer name_buffer) { + const std::string name(reinterpret_cast(name_buffer.data()), + name_buffer.size()); + LOG_DEBUG(Service_NPNS, "called, name={}", name); + R_SUCCEED(); + } + + Result GetReceiveEvent(OutCopyHandle out_event) { + LOG_DEBUG(Service_NPNS, "called"); + + *out_event = &get_receive_event->GetReadableEvent(); + R_SUCCEED(); + } + + Result GetState(Out out_state) { + LOG_INFO(Service_NPNS, "called"); + *out_state = 0; + R_SUCCEED(); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* get_receive_event; }; class INotificationReceiver : public ServiceFramework { @@ -198,8 +256,7 @@ public: class IFuture : public ServiceFramework { public: - explicit IFuture(Core::System& system_, const char* name) - : ServiceFramework{system_, name} { + explicit IFuture(Core::System& system_, const char* name) : ServiceFramework{system_, name} { // TODO: Implement functions based on documentation // Cmd 1: (No name) // Cmd 2: (No name) diff --git a/src/core/hle/service/ns/application_manager_interface.cpp b/src/core/hle/service/ns/application_manager_interface.cpp index d4a586fcd..c5ce5f04b 100644 --- a/src/core/hle/service/ns/application_manager_interface.cpp +++ b/src/core/hle/service/ns/application_manager_interface.cpp @@ -1,17 +1,19 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "core/file_sys/common_funcs.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/registered_cache.h" #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/ns/application_manager_interface.h" #include "core/hle/service/ns/content_management_interface.h" +#include "core/hle/service/ns/ns_types.h" #include "core/hle/service/ns/read_only_application_control_data_interface.h" namespace Service::NS { - IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_) + : ServiceFramework{system_, "IApplicationManagerInterface"}, service_context{system, "IApplicationManagerInterface"}, record_update_system_event{service_context}, sd_card_mount_status_event{service_context}, @@ -20,7 +22,7 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ // clang-format off static const FunctionInfo functions[] = { {0, D<&IApplicationManagerInterface::ListApplicationRecord>, "ListApplicationRecord"}, - {1, nullptr, "GenerateApplicationRecordCount"}, + {1, D<&IApplicationManagerInterface::GenerateApplicationRecordCount>, "GenerateApplicationRecordCount"}, {2, D<&IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent>, "GetApplicationRecordUpdateSystemEvent"}, {3, nullptr, "GetApplicationViewDeprecated"}, {4, nullptr, "DeleteApplicationEntity"}, @@ -123,8 +125,9 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {404, nullptr, "InvalidateApplicationControlCache"}, {405, nullptr, "ListApplicationControlCacheEntryInfo"}, {406, nullptr, "GetApplicationControlProperty"}, - {407, nullptr, "ListApplicationTitle"}, + {407, &IApplicationManagerInterface::ListApplicationTitle, "ListApplicationTitle"}, {408, nullptr, "ListApplicationIcon"}, + {419, D<&IApplicationManagerInterface::RequestDownloadApplicationControlDataInBackground>, "RequestDownloadApplicationControlDataInBackground"}, {502, nullptr, "RequestCheckGameCardRegistration"}, {503, nullptr, "RequestGameCardRegistrationGoldPoint"}, {504, nullptr, "RequestRegisterGameCard"}, @@ -134,15 +137,17 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {508, nullptr, "GetLastGameCardMountFailureResult"}, {509, nullptr, "ListApplicationIdOnGameCard"}, {510, nullptr, "GetGameCardPlatformRegion"}, - {600, nullptr, "CountApplicationContentMeta"}, + {600, D<&IApplicationManagerInterface::CountApplicationContentMeta>, "CountApplicationContentMeta"}, {601, nullptr, "ListApplicationContentMetaStatus"}, - {602, nullptr, "ListAvailableAddOnContent"}, + {602, D<&IApplicationManagerInterface::ListAvailableAddOnContent>, "ListAvailableAddOnContent"}, + {603, nullptr, "GetOwnedApplicationContentMetaStatus"}, {604, nullptr, "RegisterContentsExternalKey"}, {605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"}, {606, nullptr, "GetContentMetaStorage"}, - {607, nullptr, "ListAvailableAddOnContent"}, + {607, D<&IApplicationManagerInterface::ListAvailableAddOnContent>, "ListAvailableAddOnContent"}, {609, nullptr, "ListAvailabilityAssuredAddOnContent"}, + {610, nullptr, "GetInstalledContentMetaStorage"}, {611, nullptr, "PrepareAddOnContent"}, {700, nullptr, "PushDownloadTaskList"}, @@ -207,6 +212,7 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {1703, nullptr, "GetApplicationViewDownloadErrorContext"}, {1704, D<&IApplicationManagerInterface::GetApplicationViewWithPromotionInfo>, "GetApplicationViewWithPromotionInfo"}, {1705, nullptr, "IsPatchAutoDeletableApplication"}, + {1706, D<&IApplicationManagerInterface::GetApplicationViewDeprecated>, "GetApplicationView"}, {1800, nullptr, "IsNotificationSetupCompleted"}, {1801, nullptr, "GetLastNotificationInfoCount"}, {1802, nullptr, "ListLastNotificationInfo"}, @@ -338,7 +344,9 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {4039, nullptr, "Cmd4039"}, {4040, nullptr, "Cmd4040"}, {4041, nullptr, "Cmd4041"}, - {4042, nullptr, "Cmd4042"}, + {4041, nullptr, "Cmd4041"}, + {4042, D<&IApplicationManagerInterface::Cmd4042>, "Cmd4042"}, + {4043, nullptr, "Cmd4043"}, {4043, nullptr, "Cmd4043"}, {4044, nullptr, "Cmd4044"}, {4045, nullptr, "Cmd4045"}, @@ -347,7 +355,7 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {4050, nullptr, "Cmd4050"}, {4051, nullptr, "Cmd4051"}, {4052, nullptr, "Cmd4052"}, - {4053, nullptr, "Cmd4053"}, + {4053, D<&IApplicationManagerInterface::Cmd4053>, "Cmd4053"}, {4054, nullptr, "Cmd4054"}, {4055, nullptr, "Cmd4055"}, {4056, nullptr, "Cmd4056"}, @@ -403,58 +411,89 @@ IApplicationManagerInterface::~IApplicationManagerInterface() = default; Result IApplicationManagerInterface::GetApplicationControlData( OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, u64 application_id) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); R_RETURN(IReadOnlyApplicationControlDataInterface(system).GetApplicationControlData( out_buffer, out_actual_size, application_control_source, application_id)); } Result IApplicationManagerInterface::GetApplicationDesiredLanguage( Out out_desired_language, u32 supported_languages) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); R_RETURN(IReadOnlyApplicationControlDataInterface(system).GetApplicationDesiredLanguage( out_desired_language, supported_languages)); } Result IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode( Out out_language_code, ApplicationLanguage application_language) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); R_RETURN( IReadOnlyApplicationControlDataInterface(system).ConvertApplicationLanguageToLanguageCode( out_language_code, application_language)); } +Result IApplicationManagerInterface::GenerateApplicationRecordCount(Out out_count) { + const auto& cache = system.GetContentProviderUnion(); + const auto installed_games = cache.ListEntriesFilterOrigin( + std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); + + s32 count = 0; + for (const auto& [slot, game] : installed_games) { + if (game.title_id == 0 || game.title_id < 0x0100000000001FFFull) { + continue; + } + if ((game.title_id & 0xFFF) != 0) { + continue; // skip sub-programs + } + count++; + } + + LOG_INFO(Service_NS, "called, found {} application records", count); + *out_count = count; + R_SUCCEED(); +} + Result IApplicationManagerInterface::ListApplicationRecord( OutArray out_records, Out out_count, s32 offset) { const auto limit = out_records.size(); - LOG_WARNING(Service_NS, "(STUBBED) called"); const auto& cache = system.GetContentProviderUnion(); const auto installed_games = cache.ListEntriesFilterOrigin( std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); - size_t i = 0; - u8 ii = 24; + std::vector records; + records.reserve(installed_games.size()); for (const auto& [slot, game] : installed_games) { - if (i >= limit) { - break; - } if (game.title_id == 0 || game.title_id < 0x0100000000001FFFull) { continue; } - if (offset > 0) { - offset--; - continue; + if ((game.title_id & 0xFFF) != 0) { + continue; // skip sub-programs } ApplicationRecord record{}; record.application_id = game.title_id; record.type = ApplicationRecordType::Installed; - record.unknown = 0; // 2 = needs update - record.unknown2 = ii++; + record.attributes = 0; + record.last_updated = 0; // TODO: Implement launch timestamp tracking - out_records[i++] = record; + records.push_back(record); + } + + LOG_INFO(Service_NS, "called, offset={} limit={} total_found={}", offset, limit, + records.size()); + + // Sort by Title ID for now as a stable order + std::sort(records.begin(), records.end(), + [](const ApplicationRecord& lhs, const ApplicationRecord& rhs) { + return lhs.application_id < rhs.application_id; + }); + + size_t i = 0; + const size_t start = static_cast(std::max(0, offset)); + for (size_t j = start; j < records.size() && i < limit; ++j) { + out_records[i++] = records[j]; } *out_count = static_cast(i); @@ -463,7 +502,7 @@ Result IApplicationManagerInterface::ListApplicationRecord( Result IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent( OutCopyHandle out_event) { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); record_update_system_event.Signal(); *out_event = record_update_system_event.GetHandle(); @@ -473,29 +512,52 @@ Result IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent( Result IApplicationManagerInterface::GetGameCardMountFailureEvent( OutCopyHandle out_event) { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); *out_event = gamecard_mount_failure_event.GetHandle(); R_SUCCEED(); } Result IApplicationManagerInterface::IsAnyApplicationEntityInstalled( Out out_is_any_application_entity_installed) { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); *out_is_any_application_entity_installed = true; R_SUCCEED(); } Result IApplicationManagerInterface::GetApplicationView( + OutArray out_application_views, + InArray application_ids) { + const auto size = std::min(out_application_views.size(), application_ids.size()); + LOG_INFO(Service_NS, "called, size={}", application_ids.size()); + + for (size_t i = 0; i < size; i++) { + ApplicationViewV20 view{}; + view.application_id = application_ids[i]; + view.version = 0; // TODO: Get actual version + view.flags = 0x401f17; // Typical flags for installed app + view.unk = 0; + view.download_state = {0, 0, 0, 0, 0, {0, 0}, 0}; + view.download_progress = {0, 0, 0, 0, 0, {0, 0}, 0}; + + out_application_views[i] = view; + } + + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationViewDeprecated( OutArray out_application_views, InArray application_ids) { const auto size = std::min(out_application_views.size(), application_ids.size()); - LOG_WARNING(Service_NS, "(STUBBED) called, size={}", application_ids.size()); + LOG_INFO(Service_NS, "called (deprecated v19), size={}", application_ids.size()); for (size_t i = 0; i < size; i++) { ApplicationView view{}; view.application_id = application_ids[i]; - view.unk = 0x70000; + view.version = 0; view.flags = 0x401f17; + view.download_state = {0, 0, 0, 0, 0, {0, 0}, 0}; + view.download_progress = {0, 0, 0, 0, 0, {0, 0}, 0}; out_application_views[i] = view; } @@ -504,21 +566,39 @@ Result IApplicationManagerInterface::GetApplicationView( } Result IApplicationManagerInterface::GetApplicationViewWithPromotionInfo( - OutArray out_application_views, + OutBuffer out_buffer, Out out_count, InArray application_ids) { - const auto size = std::min(out_application_views.size(), application_ids.size()); - LOG_WARNING(Service_NS, "(STUBBED) called, size={}", application_ids.size()); + const auto size = application_ids.size(); + LOG_INFO(Service_NS, "called, size={}", size); - for (size_t i = 0; i < size; i++) { - ApplicationViewWithPromotionInfo view{}; - view.view.application_id = application_ids[i]; - view.view.unk = 0x70000; - view.view.flags = 0x401f17; - view.promotion = {}; + // Using 0x78 per entry (V20 + PromotionInfo) + constexpr size_t entry_size = 0x58 + 0x20; + const size_t limit = out_buffer.size() / entry_size; + const size_t actual_count = std::min(size, limit); - out_application_views[i] = view; + for (size_t i = 0; i < actual_count; i++) { + ApplicationViewV20 view{}; + view.application_id = application_ids[i]; + view.version = 0; + view.flags = 0x401f17; + view.unk = 0; + + PromotionInfo promotion{}; + + const size_t offset = i * entry_size; + std::memcpy(out_buffer.data() + offset, &view, sizeof(view)); + std::memcpy(out_buffer.data() + offset + sizeof(view), &promotion, sizeof(promotion)); } + *out_count = static_cast(actual_count); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::RequestDownloadApplicationControlDataInBackground( + u64 control_source, u64 application_id) { + LOG_INFO(Service_NS, "called, control_source={}, application_id={:016X}", control_source, + application_id); + // Success allows QLaunch to continue even if we don't actually download anything R_SUCCEED(); } @@ -545,32 +625,32 @@ Result IApplicationManagerInterface::GetApplicationRightsOnClient( } Result IApplicationManagerInterface::CheckSdCardMountStatus() { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); R_RETURN(IContentManagementInterface(system).CheckSdCardMountStatus()); } Result IApplicationManagerInterface::GetSdCardMountStatusChangedEvent( OutCopyHandle out_event) { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); *out_event = sd_card_mount_status_event.GetHandle(); R_SUCCEED(); } Result IApplicationManagerInterface::GetFreeSpaceSize(Out out_free_space_size, FileSys::StorageId storage_id) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); R_RETURN(IContentManagementInterface(system).GetFreeSpaceSize(out_free_space_size, storage_id)); } Result IApplicationManagerInterface::GetGameCardUpdateDetectionEvent( OutCopyHandle out_event) { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); *out_event = gamecard_update_detection_event.GetHandle(); R_SUCCEED(); } Result IApplicationManagerInterface::ResumeAll() { - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_INFO(Service_NS, "called"); R_SUCCEED(); } @@ -624,4 +704,85 @@ Result IApplicationManagerInterface::Cmd4088(Out out_result) { R_SUCCEED(); } +Result IApplicationManagerInterface::Cmd4042(Out out_result) { + LOG_DEBUG(Service_NS, "(STUBBED) called [20.0.0+]"); + *out_result = 0; + R_SUCCEED(); +} + +Result IApplicationManagerInterface::CountApplicationContentMeta(Out out_count, + u64 application_id) { + LOG_DEBUG(Service_NS, "called, application_id={:016X}", application_id); + + const auto& cache = system.GetContentProviderUnion(); + const auto installed_aocs = cache.ListEntriesFilterOrigin(std::nullopt, FileSys::TitleType::AOC, + FileSys::ContentRecordType::Data); + + u32 count = 0; + for (const auto& [slot, aoc] : installed_aocs) { + if (FileSys::GetBaseTitleID(aoc.title_id) == application_id) { + count++; + } + } + + LOG_DEBUG(Service_NS, "CountApplicationContentMeta found {} AOCS for app {:016X}", count, + application_id); + + *out_count = count; + R_SUCCEED(); +} + +Result IApplicationManagerInterface::ListAvailableAddOnContent( + OutArray out_addons, Out out_count, u32 offset, + u64 application_id) { + const auto limit = out_addons.size(); + LOG_DEBUG(Service_NS, "called, offset={}, limit={}, application_id={:016X}", offset, limit, + application_id); + + const auto& cache = system.GetContentProviderUnion(); + const auto installed_aocs = cache.ListEntriesFilterOrigin(std::nullopt, FileSys::TitleType::AOC, + FileSys::ContentRecordType::Data); + + std::vector aocs; + aocs.reserve(installed_aocs.size()); + + LOG_DEBUG(Service_NS, "Scanning {} installed AOCs for application {:016X}", + installed_aocs.size(), application_id); + + for (const auto& [slot, aoc] : installed_aocs) { + const auto base_id = FileSys::GetBaseTitleID(aoc.title_id); + if (base_id == application_id) { + LOG_DEBUG(Service_NS, "Found match! AOC ID: {:016X}, Base ID: {:016X}", aoc.title_id, + base_id); + aocs.push_back(static_cast(FileSys::GetAOCID(aoc.title_id))); + } else { + // Uncomment for even more verbose logging if needed + // LOG_DEBUG(Service_NS, "Skipping AOC ID: {:016X} (Base: {:016X})", aoc.title_id, + // base_id); + } + } + + std::sort(aocs.begin(), aocs.end()); + + size_t i = 0; + const size_t start = static_cast(std::max(0, offset)); + for (size_t j = start; j < aocs.size() && i < limit; ++j) { + out_addons[i++] = aocs[j]; + } + + *out_count = static_cast(i); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::Cmd4053() { + + LOG_WARNING(Service_NS, "(STUBBED) called."); + R_SUCCEED(); +} + +void IApplicationManagerInterface::ListApplicationTitle(HLERequestContext& ctx) { + LOG_DEBUG(Service_NS, "called"); + IReadOnlyApplicationControlDataInterface(system).ListApplicationTitle(ctx); +} + } // namespace Service::NS diff --git a/src/core/hle/service/ns/application_manager_interface.h b/src/core/hle/service/ns/application_manager_interface.h index 378a9131b..f5ca54d59 100644 --- a/src/core/hle/service/ns/application_manager_interface.h +++ b/src/core/hle/service/ns/application_manager_interface.h @@ -24,17 +24,23 @@ public: u32 supported_languages); Result ConvertApplicationLanguageToLanguageCode(Out out_language_code, ApplicationLanguage application_language); + Result GenerateApplicationRecordCount(Out out_count); Result ListApplicationRecord(OutArray out_records, Out out_count, s32 offset); Result GetApplicationRecordUpdateSystemEvent(OutCopyHandle out_event); Result GetGameCardMountFailureEvent(OutCopyHandle out_event); Result IsAnyApplicationEntityInstalled(Out out_is_any_application_entity_installed); Result GetApplicationView( + OutArray out_application_views, + InArray application_ids); + Result GetApplicationViewDeprecated( OutArray out_application_views, InArray application_ids); Result GetApplicationViewWithPromotionInfo( - OutArray out_application_views, + OutBuffer out_buffer, Out out_count, InArray application_ids); + Result RequestDownloadApplicationControlDataInBackground(u64 control_source, + u64 application_id); Result GetApplicationRightsOnClient( OutArray out_rights, Out out_count, u32 flags, u64 application_id, Uid account_id); @@ -53,7 +59,15 @@ public: // [20.0.0+] Stub functions for QLaunch compatibility Result Cmd4022(Out out_result); Result Cmd4023(Out out_result); + Result Cmd4042(Out out_result); + Result Cmd4053(); Result Cmd4088(Out out_result); + // [1.0.0+] + Result CountApplicationContentMeta(Out out_count, u64 application_id); + Result ListAvailableAddOnContent(OutArray out_addons, + Out out_count, u32 offset, u64 application_id); + + void ListApplicationTitle(HLERequestContext& ctx); private: KernelHelpers::ServiceContext service_context; diff --git a/src/core/hle/service/ns/ns_types.h b/src/core/hle/service/ns/ns_types.h index 2dd664c4e..8859b90e5 100644 --- a/src/core/hle/service/ns/ns_types.h +++ b/src/core/hle/service/ns/ns_types.h @@ -29,31 +29,48 @@ enum class BackgroundNetworkUpdateState : u8 { Ready, }; +/// ApplicationDownloadState +struct ApplicationDownloadState { + u64 downloaded_size; + u64 total_size; + u32 unk_x10; + u8 state; + u8 unk_x15; + std::array unk_x16; + u64 unk_x18; +}; +static_assert(sizeof(ApplicationDownloadState) == 0x20, + "ApplicationDownloadState has incorrect size."); + struct ApplicationRecord { u64 application_id; ApplicationRecordType type; - u8 unknown; + u8 attributes; INSERT_PADDING_BYTES_NOINIT(0x6); - u8 unknown2; - INSERT_PADDING_BYTES_NOINIT(0x7); + s64 last_updated; }; static_assert(sizeof(ApplicationRecord) == 0x18, "ApplicationRecord has incorrect size."); -/// ApplicationView -struct ApplicationView { - u64 application_id; ///< ApplicationId. - u32 unk; ///< Unknown. - u32 flags; ///< Flags. - std::array unk_x10; ///< Unknown. - u32 unk_x20; ///< Unknown. - u16 unk_x24; ///< Unknown. - std::array unk_x26; ///< Unknown. - std::array unk_x28; ///< Unknown. - std::array unk_x30; ///< Unknown. - u32 unk_x40; ///< Unknown. - u8 unk_x44; ///< Unknown. - std::array unk_x45; ///< Unknown. +struct ApplicationViewV19 { + u64 application_id; + u32 version; + u32 flags; + ApplicationDownloadState download_state; + ApplicationDownloadState download_progress; }; +static_assert(sizeof(ApplicationViewV19) == 0x50, "ApplicationViewV19 has incorrect size."); + +struct ApplicationViewV20 { + u64 application_id; + u32 version; + u32 flags; + u32 unk; + ApplicationDownloadState download_state; + ApplicationDownloadState download_progress; +}; +static_assert(sizeof(ApplicationViewV20) == 0x58, "ApplicationViewV20 has incorrect size."); + +using ApplicationView = ApplicationViewV19; static_assert(sizeof(ApplicationView) == 0x50, "ApplicationView has incorrect size."); struct ApplicationRightsOnClient { diff --git a/src/core/hle/service/ns/query_service.cpp b/src/core/hle/service/ns/query_service.cpp index 138400541..41bc565b4 100644 --- a/src/core/hle/service/ns/query_service.cpp +++ b/src/core/hle/service/ns/query_service.cpp @@ -1,8 +1,12 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include "common/logging/log.h" #include "common/uuid.h" +#include "core/core.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/registered_cache.h" #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/ns/query_service.h" #include "core/hle/service/service.h" @@ -26,12 +30,12 @@ IQueryService::IQueryService(Core::System& system_) : ServiceFramework{system_, {11, nullptr, "QueryAccountPlayEvent"}, {12, nullptr, "GetAvailableAccountPlayEventRange"}, {13, nullptr, "QueryApplicationPlayStatisticsForSystemV0"}, - {14, nullptr, "QueryRecentlyPlayedApplication"}, + {14, D<&IQueryService::QueryRecentlyPlayedApplication>, "QueryRecentlyPlayedApplication"}, {15, nullptr, "GetRecentlyPlayedApplicationUpdateEvent"}, {16, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystemV0"}, {17, nullptr, "QueryLastPlayTime"}, - {18, nullptr, "QueryApplicationPlayStatisticsForSystem"}, - {19, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystem"}, + {18, D<&IQueryService::QueryApplicationPlayStatisticsForSystem>, "QueryApplicationPlayStatisticsForSystem"}, + {19, D<&IQueryService::QueryApplicationPlayStatisticsByUserAccountIdForSystem>, "QueryApplicationPlayStatisticsByUserAccountIdForSystem"}, }; // clang-format on @@ -53,4 +57,66 @@ Result IQueryService::QueryPlayStatisticsByApplicationIdAndUserAccountId( R_SUCCEED(); } +Result IQueryService::QueryRecentlyPlayedApplication( + Out out_count, OutArray out_applications, Uid user_id) { + const auto limit = out_applications.size(); + LOG_INFO(Service_NS, "called. user_id={}, limit={}", user_id.uuid.FormattedString(), limit); + + const auto& cache = system.GetContentProviderUnion(); + auto installed_games = + cache.ListEntriesFilter(std::nullopt, FileSys::ContentRecordType::Program, std::nullopt); + + // Filter and sort + std::vector applications; + for (const auto& entry : installed_games) { + if (entry.title_id == 0 || entry.title_id < 0x0100000000001FFFull) { + continue; + } + applications.push_back(entry.title_id); + } + + // Sort by Title ID for now as a stable order + std::sort(applications.begin(), applications.end()); + applications.erase(std::unique(applications.begin(), applications.end()), applications.end()); + + const auto count = std::min(limit, applications.size()); + LOG_INFO(Service_NS, "Returning {} applications out of {} found.", count, applications.size()); + + for (size_t i = 0; i < count; i++) { + LOG_INFO(Service_NS, " [{}] TitleID: {:016X}", i, applications[i]); + out_applications[i] = applications[i]; + } + + *out_count = static_cast(count); + R_SUCCEED(); +} + +Result IQueryService::QueryApplicationPlayStatisticsForSystem( + Out out_entries, u8 flag, + OutArray out_stats, + InArray application_ids) { + const size_t count = std::min(out_stats.size(), application_ids.size()); + s32 written = 0; + for (size_t i = 0; i < count; ++i) { + const u64 app_id = application_ids[i]; + ApplicationPlayStatistics stats{}; + stats.application_id = app_id; + stats.play_time_ns = 0; // TODO: Implement play time tracking + stats.launch_count = 1; // Default to 1 for now + out_stats[i] = stats; + ++written; + } + *out_entries = written; + LOG_INFO(Service_NS, "called, entries={} flag={}", written, flag); + R_SUCCEED(); +} + +Result IQueryService::QueryApplicationPlayStatisticsByUserAccountIdForSystem( + Out out_entries, u8 flag, Common::UUID user_id, + OutArray out_stats, + InArray application_ids) { + LOG_INFO(Service_NS, "called, user_id={}", user_id.FormattedString()); + return QueryApplicationPlayStatisticsForSystem(out_entries, flag, out_stats, application_ids); +} + } // namespace Service::NS diff --git a/src/core/hle/service/ns/query_service.h b/src/core/hle/service/ns/query_service.h index c4c82b752..d317cb084 100644 --- a/src/core/hle/service/ns/query_service.h +++ b/src/core/hle/service/ns/query_service.h @@ -23,6 +23,18 @@ struct PlayStatistics { }; static_assert(sizeof(PlayStatistics) == 0x28, "PlayStatistics is an invalid size"); +struct LastPlayTime { + INSERT_PADDING_BYTES_NOINIT(0x8); // Likely needs padding for buffer alignment if used +}; + +struct ApplicationPlayStatistics { + u64 application_id{}; + u64 play_time_ns{}; + u64 launch_count{}; +}; +static_assert(sizeof(ApplicationPlayStatistics) == 0x18, + "ApplicationPlayStatistics is an invalid size"); + class IQueryService final : public ServiceFramework { public: explicit IQueryService(Core::System& system_); @@ -31,6 +43,20 @@ public: private: Result QueryPlayStatisticsByApplicationIdAndUserAccountId( Out out_play_statistics, bool unknown, u64 application_id, Uid account_id); + + Result QueryRecentlyPlayedApplication(Out out_count, + OutArray out_applications, + Uid user_id); + + Result QueryApplicationPlayStatisticsForSystem( + Out out_entries, u8 flag, + OutArray out_stats, + InArray application_ids); + + Result QueryApplicationPlayStatisticsByUserAccountIdForSystem( + Out out_entries, u8 flag, Common::UUID user_id, + OutArray out_stats, + InArray application_ids); }; } // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_control_data_interface.cpp b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp index 9b2ca94a4..d6a336bae 100644 --- a/src/core/hle/service/ns/read_only_application_control_data_interface.cpp +++ b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp @@ -1,11 +1,30 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" + +#define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_STATIC +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#define STB_IMAGE_RESIZE_STATIC +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STB_IMAGE_WRITE_STATIC +#include +#include +#include + +#pragma GCC diagnostic pop + +#include +#include #include "common/settings.h" #include "core/file_sys/control_metadata.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/vfs/vfs.h" +#include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/kernel_helpers.h" #include "core/hle/service/ns/language.h" #include "core/hle/service/ns/ns_results.h" #include "core/hle/service/ns/read_only_application_control_data_interface.h" @@ -13,6 +32,113 @@ namespace Service::NS { +namespace { + +void JPGToMemory(void* context, void* data, int size) { + auto* buffer = static_cast*>(context); + const auto* char_data = static_cast(data); + buffer->insert(buffer->end(), char_data, char_data + size); +} + +void SanitizeJPEGImageSize(std::vector& image) { + constexpr std::size_t max_jpeg_image_size = 0x20000; + constexpr int profile_dimensions = 174; // for grid view thingy + int original_width, original_height, color_channels; + + auto* plain_image = + stbi_load_from_memory(image.data(), static_cast(image.size()), &original_width, + &original_height, &color_channels, STBI_rgb); + + if (plain_image == nullptr) { + LOG_ERROR(Service_NS, "Failed to load JPEG for sanitization."); + return; + } + + if (original_width != profile_dimensions || original_height != profile_dimensions) { + std::vector out_image(profile_dimensions * profile_dimensions * STBI_rgb); + stbir_resize_uint8_srgb(plain_image, original_width, original_height, 0, out_image.data(), + profile_dimensions, profile_dimensions, 0, STBI_rgb, 0, + STBIR_FILTER_BOX); + image.clear(); + if (!stbi_write_jpg_to_func(JPGToMemory, &image, profile_dimensions, profile_dimensions, + STBI_rgb, out_image.data(), 90)) { + LOG_ERROR(Service_NS, "Failed to resize the user provided image."); + } + } + + stbi_image_free(plain_image); + + if (image.size() > max_jpeg_image_size) { + image.resize(max_jpeg_image_size); + } +} + +} // namespace + +// IAsyncValue implementation for ListApplicationTitle +// https://switchbrew.org/wiki/NS_services#ListApplicationTitle +class IAsyncValueForListApplicationTitle final + : public ServiceFramework { +public: + explicit IAsyncValueForListApplicationTitle(Core::System& system_, s32 offset, s32 size) + : ServiceFramework{system_, "IAsyncValue"}, service_context{system_, "IAsyncValue"}, + data_offset{offset}, data_size{size} { + static const FunctionInfo functions[] = { + {0, &IAsyncValueForListApplicationTitle::GetSize, "GetSize"}, + {1, &IAsyncValueForListApplicationTitle::Get, "Get"}, + {2, &IAsyncValueForListApplicationTitle::Cancel, "Cancel"}, + {3, &IAsyncValueForListApplicationTitle::GetErrorContext, "GetErrorContext"}, + }; + RegisterHandlers(functions); + + completion_event = service_context.CreateEvent("IAsyncValue:Completion"); + completion_event->GetReadableEvent().Signal(); + } + + ~IAsyncValueForListApplicationTitle() override { + service_context.CloseEvent(completion_event); + } + + Kernel::KReadableEvent& ReadableEvent() const { + return completion_event->GetReadableEvent(); + } + +private: + void GetSize(HLERequestContext& ctx) { + LOG_DEBUG(Service_NS, "called"); + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); + rb.Push(data_size); + } + + void Get(HLERequestContext& ctx) { + LOG_DEBUG(Service_NS, "called"); + std::vector buffer(sizeof(s32)); + std::memcpy(buffer.data(), &data_offset, sizeof(s32)); + ctx.WriteBuffer(buffer); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void Cancel(HLERequestContext& ctx) { + LOG_DEBUG(Service_NS, "called"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void GetErrorContext(HLERequestContext& ctx) { + LOG_DEBUG(Service_NS, "called"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* completion_event{}; + s32 data_offset; + s32 data_size; +}; + IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterface( Core::System& system_) : ServiceFramework{system_, "IReadOnlyApplicationControlDataInterface"} { @@ -23,6 +149,9 @@ IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterfa {2, D<&IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLanguageCode>, "ConvertApplicationLanguageToLanguageCode"}, {3, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, {4, nullptr, "SelectApplicationDesiredLanguage"}, + {5, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData2>, "GetApplicationControlData"}, + {13, &IReadOnlyApplicationControlDataInterface::ListApplicationTitle, "ListApplicationTitle"}, + {19, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData3>, "GetApplicationControlData"}, }; // clang-format on @@ -119,4 +248,191 @@ Result IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLan R_SUCCEED(); } +Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData2( + OutBuffer out_buffer, Out out_total_size, + ApplicationControlSource application_control_source, u8 flag1, u8 flag2, u64 application_id) { + LOG_INFO(Service_NS, + "called with control_source={}, flags=({:02X},{:02X}), application_id={:016X}", + application_control_source, flag1, flag2, application_id); + + const FileSys::PatchManager pm{application_id, system.GetFileSystemController(), + system.GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto size = out_buffer.size(); + + const auto nacp_size = sizeof(FileSys::RawNACP); + + if (size < nacp_size) { + LOG_ERROR(Service_NS, "output buffer is too small! (actual={:016X}, expected_min={:08X})", + size, nacp_size); + R_THROW(ResultUnknown); + } + + if (control.first != nullptr) { + const auto bytes = control.first->GetRawBytes(); + const auto copy_len = + (std::min)(static_cast(bytes.size()), static_cast(nacp_size)); + std::memcpy(out_buffer.data(), bytes.data(), copy_len); + if (copy_len < nacp_size) { + std::memset(out_buffer.data() + copy_len, 0, nacp_size - copy_len); + } + } else { + LOG_WARNING(Service_NS, "missing NACP data for application_id={:016X}", application_id); + std::memset(out_buffer.data(), 0, nacp_size); + } + + const auto icon_area_size = size - nacp_size; + std::vector final_icon_data; + + if (control.second != nullptr) { + size_t full_size = control.second->GetSize(); + if (full_size > 0) { + final_icon_data.resize(full_size); + control.second->Read(final_icon_data.data(), full_size); + + if (flag1 == 1) { + SanitizeJPEGImageSize(final_icon_data); + } + } + } + + size_t available_icon_bytes = final_icon_data.size(); + + if (icon_area_size > 0) { + const size_t to_copy = (std::min)(available_icon_bytes, icon_area_size); + + if (to_copy > 0) { + std::memcpy(out_buffer.data() + nacp_size, final_icon_data.data(), to_copy); + } + + if (to_copy < icon_area_size) { + std::memset(out_buffer.data() + nacp_size + to_copy, 0, icon_area_size - to_copy); + } + } + + const u32 total_available = static_cast(nacp_size + available_icon_bytes); + + *out_total_size = (static_cast(total_available) << 32) | static_cast(flag1); + R_SUCCEED(); +} + +void IReadOnlyApplicationControlDataInterface::ListApplicationTitle(HLERequestContext& ctx) { + const auto app_ids_buffer = ctx.ReadBuffer(); + const size_t app_count = app_ids_buffer.size() / sizeof(u64); + + std::vector application_ids(app_count); + if (app_count > 0) { + std::memcpy(application_ids.data(), app_ids_buffer.data(), app_count * sizeof(u64)); + } + + auto t_mem_obj = ctx.GetObjectFromHandle(ctx.GetCopyHandle(0)); + auto* t_mem = t_mem_obj.GetPointerUnsafe(); + + constexpr size_t title_entry_size = sizeof(FileSys::LanguageEntry); + const size_t total_data_size = app_count * title_entry_size; + + constexpr s32 data_offset = 0; + + if (t_mem != nullptr && app_count > 0) { + auto& memory = system.ApplicationMemory(); + const auto t_mem_address = t_mem->GetSourceAddress(); + + for (size_t i = 0; i < app_count; ++i) { + const u64 app_id = application_ids[i]; + const FileSys::PatchManager pm{app_id, system.GetFileSystemController(), + system.GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + + FileSys::LanguageEntry entry{}; + if (control.first != nullptr) { + entry = control.first->GetLanguageEntry(); + } + + const size_t offset = i * title_entry_size; + memory.WriteBlock(t_mem_address + offset, &entry, title_entry_size); + } + } + + auto async_value = std::make_shared( + system, data_offset, static_cast(total_data_size)); + + IPC::ResponseBuilder rb{ctx, 2, 1, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(async_value->ReadableEvent()); + rb.PushIpcInterface(std::move(async_value)); +} + +Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData3( + OutBuffer out_buffer, Out out_flags_a, Out out_flags_b, + Out out_actual_size, ApplicationControlSource application_control_source, u8 flag1, + u8 flag2, u64 application_id) { + LOG_INFO(Service_NS, + "called with control_source={}, flags=({:02X},{:02X}), application_id={:016X}", + application_control_source, flag1, flag2, application_id); + + const FileSys::PatchManager pm{application_id, system.GetFileSystemController(), + system.GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto size = out_buffer.size(); + + const auto nacp_size = sizeof(FileSys::RawNACP); + + if (size < nacp_size) { + LOG_ERROR(Service_NS, "output buffer is too small! (actual={:016X}, expected_min={:08X})", + size, nacp_size); + R_THROW(ResultUnknown); + } + + if (control.first != nullptr) { + const auto bytes = control.first->GetRawBytes(); + const auto copy_len = + (std::min)(static_cast(bytes.size()), static_cast(nacp_size)); + std::memcpy(out_buffer.data(), bytes.data(), copy_len); + if (copy_len < nacp_size) { + std::memset(out_buffer.data() + copy_len, 0, nacp_size - copy_len); + } + } else { + LOG_WARNING(Service_NS, "missing NACP data for application_id={:016X}, defaulting to zero", + application_id); + std::memset(out_buffer.data(), 0, nacp_size); + } + + const auto icon_area_size = size - nacp_size; + std::vector final_icon_data; + + if (control.second != nullptr) { + size_t full_size = control.second->GetSize(); + if (full_size > 0) { + final_icon_data.resize(full_size); + control.second->Read(final_icon_data.data(), full_size); + // TODO: Implement SanitizeJPEGImageSize(final_icon_data) if flag1 == 1 + } + } + + size_t available_icon_bytes = final_icon_data.size(); + + if (icon_area_size > 0) { + const size_t to_copy = (std::min)(available_icon_bytes, icon_area_size); + if (to_copy > 0) { + std::memcpy(out_buffer.data() + nacp_size, final_icon_data.data(), to_copy); + } + if (to_copy < icon_area_size) { + std::memset(out_buffer.data() + nacp_size + to_copy, 0, icon_area_size - to_copy); + } + } else { + std::memset(out_buffer.data() + nacp_size, 0, icon_area_size); + } + + const u32 actual_total_size = static_cast(nacp_size + available_icon_bytes); + + // Out 1: always 0x10001 (likely presents flags: Bit0=Icon, Bit16=NACP) + // Out 2: reflects flag1 application (0 if flag1=0, 0x10001 if flag1=1) + // Out 3: The actual size of data + *out_flags_a = 0x10001; + *out_flags_b = (flag1 == 1) ? 0x10001 : 0; + *out_actual_size = actual_total_size; + + R_SUCCEED(); +} + } // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_control_data_interface.h b/src/core/hle/service/ns/read_only_application_control_data_interface.h index ac099435a..3d301068b 100644 --- a/src/core/hle/service/ns/read_only_application_control_data_interface.h +++ b/src/core/hle/service/ns/read_only_application_control_data_interface.h @@ -16,7 +16,6 @@ public: explicit IReadOnlyApplicationControlDataInterface(Core::System& system_); ~IReadOnlyApplicationControlDataInterface() override; -public: Result GetApplicationControlData(OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, @@ -25,6 +24,16 @@ public: u32 supported_languages); Result ConvertApplicationLanguageToLanguageCode(Out out_language_code, ApplicationLanguage application_language); + Result GetApplicationControlData2(OutBuffer out_buffer, + Out out_total_size, + ApplicationControlSource application_control_source, u8 flag1, + u8 flag2, u64 application_id); + void ListApplicationTitle(HLERequestContext& ctx); + Result GetApplicationControlData3(OutBuffer out_buffer, + Out out_flags_a, Out out_flags_b, + Out out_actual_size, + ApplicationControlSource application_control_source, u8 flag1, + u8 flag2, u64 application_id); }; } // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_record_interface.cpp b/src/core/hle/service/ns/read_only_application_record_interface.cpp index 816a1e1dc..cadbd939f 100644 --- a/src/core/hle/service/ns/read_only_application_record_interface.cpp +++ b/src/core/hle/service/ns/read_only_application_record_interface.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/application_manager_interface.h" #include "core/hle/service/ns/read_only_application_record_interface.h" namespace Service::NS { @@ -13,6 +14,8 @@ IReadOnlyApplicationRecordInterface::IReadOnlyApplicationRecordInterface(Core::S {1, nullptr, "NotifyApplicationFailure"}, {2, D<&IReadOnlyApplicationRecordInterface::IsDataCorruptedResult>, "IsDataCorruptedResult"}, + {3, D<&IReadOnlyApplicationRecordInterface::ListApplicationRecord>, + "ListApplicationRecord"}, }; // clang-format on @@ -35,4 +38,16 @@ Result IReadOnlyApplicationRecordInterface::IsDataCorruptedResult( R_SUCCEED(); } +Result IReadOnlyApplicationRecordInterface::ListApplicationRecord( + OutArray out_records, Out out_count, + s32 entry_offset) { + LOG_INFO(Service_NS, + "delegating to IApplicationManagerInterface::ListApplicationRecord, offset={} " + "limit={}", + entry_offset, out_records.size()); + + R_RETURN(IApplicationManagerInterface(system).ListApplicationRecord(out_records, out_count, + entry_offset)); +} + } // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_record_interface.h b/src/core/hle/service/ns/read_only_application_record_interface.h index d06e8f5e6..688313d58 100644 --- a/src/core/hle/service/ns/read_only_application_record_interface.h +++ b/src/core/hle/service/ns/read_only_application_record_interface.h @@ -4,6 +4,7 @@ #pragma once #include "core/hle/service/cmif_types.h" +#include "core/hle/service/ns/ns_types.h" #include "core/hle/service/service.h" namespace Service::NS { @@ -17,6 +18,8 @@ public: private: Result HasApplicationRecord(Out out_has_application_record, u64 program_id); Result IsDataCorruptedResult(Out out_is_data_corrupted_result, Result result); + Result ListApplicationRecord(OutArray out_records, + Out out_count, s32 entry_offset); }; } // namespace Service::NS diff --git a/src/core/hle/service/ns/service_getter_interface.cpp b/src/core/hle/service/ns/service_getter_interface.cpp index 1a3dd7166..67f153602 100644 --- a/src/core/hle/service/ns/service_getter_interface.cpp +++ b/src/core/hle/service/ns/service_getter_interface.cpp @@ -42,77 +42,77 @@ IServiceGetterInterface::~IServiceGetterInterface() = default; Result IServiceGetterInterface::GetDynamicRightsInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetReadOnlyApplicationControlDataInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetReadOnlyApplicationRecordInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetECommerceInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetApplicationVersionInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetFactoryResetInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetAccountProxyInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetApplicationManagerInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetDownloadTaskInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetContentManagementInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } Result IServiceGetterInterface::GetDocumentInterface( Out> out_interface) { - LOG_DEBUG(Service_NS, "called"); + LOG_INFO(Service_NS, "called"); *out_interface = std::make_shared(system); R_SUCCEED(); } diff --git a/src/core/hle/service/nvdrv/core/container.cpp b/src/core/hle/service/nvdrv/core/container.cpp index 9edce03f6..c7aed06a4 100644 --- a/src/core/hle/service/nvdrv/core/container.cpp +++ b/src/core/hle/service/nvdrv/core/container.cpp @@ -16,8 +16,9 @@ namespace Service::Nvidia::NvCore { -Session::Session(SessionId id_, Kernel::KProcess* process_, Core::Asid asid_) - : id{id_}, process{process_}, asid{asid_}, has_preallocated_area{}, mapper{}, is_active{} {} +Session::Session(SessionId id_, Kernel::KProcess* process_, u64 pid_, Core::Asid asid_) + : id{id_}, process{process_}, pid{pid_}, asid{asid_}, has_preallocated_area{}, mapper{}, + is_active{} {} Session::~Session() = default; @@ -49,8 +50,39 @@ SessionId Container::OpenSession(Kernel::KProcess* process) { continue; } if (session.process == process) { - session.ref_count++; - return session.id; + if (session.pid != process->GetProcessId()) { + LOG_WARNING(Service_NVDRV, + "Container::OpenSession: Stale session detected! PID mismatch (old={}, " + "new={}). Marking as inactive.", + session.pid, process->GetProcessId()); + // Force close stale session logic + // NOTE: We do NOT unmap handles or free memory here because it causes + // KSynchronizationObject::UnlinkNode segfaults if threads are still waiting on + // events associated with this session. We effectively leak the old session's + // resources to ensure stability. + // impl->file.UnmapAllHandles(session.id); + + // auto& smmu_mgr = impl->host1x.MemoryManager(); + /* + if (session.has_preallocated_area) { + const DAddr region_start = session.mapper->GetRegionStart(); + const size_t region_size = session.mapper->GetRegionSize(); + session.mapper.reset(); + smmu_mgr.Free(region_start, region_size); + session.has_preallocated_area = false; + } + */ + session.is_active = false; + session.ref_count = 0; + // smmu_mgr.UnregisterProcess(session.asid); + // impl->id_pool.emplace_front(session.id.id); + // Continue searching to clean up other stale sessions if any? + // Or proceed to create new one. Stale one is now inactive. + continue; + } else { + session.ref_count++; + return session.id; + } } } size_t new_id{}; @@ -60,10 +92,10 @@ SessionId Container::OpenSession(Kernel::KProcess* process) { if (!impl->id_pool.empty()) { new_id = impl->id_pool.front(); impl->id_pool.pop_front(); - impl->sessions[new_id] = Session{SessionId{new_id}, process, asid}; + impl->sessions[new_id] = Session{SessionId{new_id}, process, process->GetProcessId(), asid}; } else { new_id = impl->new_ids++; - impl->sessions.emplace_back(SessionId{new_id}, process, asid); + impl->sessions.emplace_back(SessionId{new_id}, process, process->GetProcessId(), asid); } auto& session = impl->sessions[new_id]; session.is_active = true; diff --git a/src/core/hle/service/nvdrv/core/container.h b/src/core/hle/service/nvdrv/core/container.h index f159ced09..b7099ce36 100644 --- a/src/core/hle/service/nvdrv/core/container.h +++ b/src/core/hle/service/nvdrv/core/container.h @@ -32,7 +32,7 @@ struct SessionId { }; struct Session { - Session(SessionId id_, Kernel::KProcess* process_, Core::Asid asid_); + Session(SessionId id_, Kernel::KProcess* process_, u64 pid_, Core::Asid asid_); ~Session(); Session(const Session&) = delete; @@ -42,6 +42,7 @@ struct Session { SessionId id; Kernel::KProcess* process; + u64 pid; Core::Asid asid; bool has_preallocated_area{}; std::unique_ptr mapper{}; diff --git a/src/core/hle/service/nvdrv/nvdrv_interface.cpp b/src/core/hle/service/nvdrv/nvdrv_interface.cpp index 258970fd5..b2b5dfee1 100644 --- a/src/core/hle/service/nvdrv/nvdrv_interface.cpp +++ b/src/core/hle/service/nvdrv/nvdrv_interface.cpp @@ -160,7 +160,6 @@ void NVDRV::Initialize(HLERequestContext& ctx) { }; if (is_initialized) { - // No need to initialize again return; } diff --git a/src/core/hle/service/nvnflinger/display.h b/src/core/hle/service/nvnflinger/display.h index 40aa59787..e89fcc8fd 100644 --- a/src/core/hle/service/nvnflinger/display.h +++ b/src/core/hle/service/nvnflinger/display.h @@ -12,7 +12,7 @@ struct Layer { explicit Layer(std::shared_ptr buffer_item_consumer_, s32 consumer_id_) : buffer_item_consumer(std::move(buffer_item_consumer_)), consumer_id(consumer_id_), - blending(LayerBlending::None), visible(true) {} + blending(LayerBlending::None), visible(true), z_index(0), is_overlay(false) {} ~Layer() { buffer_item_consumer->Abandon(); } @@ -21,6 +21,8 @@ struct Layer { s32 consumer_id; LayerBlending blending; bool visible; + s32 z_index; + bool is_overlay; }; struct LayerStack { diff --git a/src/core/hle/service/nvnflinger/hardware_composer.cpp b/src/core/hle/service/nvnflinger/hardware_composer.cpp index f2dfe85a9..0e662d9d9 100644 --- a/src/core/hle/service/nvnflinger/hardware_composer.cpp +++ b/src/core/hle/service/nvnflinger/hardware_composer.cpp @@ -83,7 +83,7 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display, .width = igbp_buffer.Width(), .height = igbp_buffer.Height(), .stride = igbp_buffer.Stride(), - .z_index = 0, + .z_index = layer->z_index, .blending = layer->blending, .transform = static_cast(item.transform), .crop_rect = item.crop, diff --git a/src/core/hle/service/nvnflinger/surface_flinger.cpp b/src/core/hle/service/nvnflinger/surface_flinger.cpp index 8362b65e5..18bf55f5a 100644 --- a/src/core/hle/service/nvnflinger/surface_flinger.cpp +++ b/src/core/hle/service/nvnflinger/surface_flinger.cpp @@ -121,6 +121,13 @@ std::shared_ptr SurfaceFlinger::FindLayer(s32 consumer_binder_id) { return nullptr; } +void SurfaceFlinger::SetLayerIsOverlay(s32 consumer_binder_id, bool is_overlay) { + if (const auto layer = this->FindLayer(consumer_binder_id); layer != nullptr) { + layer->is_overlay = is_overlay; + LOG_DEBUG(Service_VI, "Layer {} marked as overlay: {}", consumer_binder_id, is_overlay); + } +} + void SurfaceFlinger::CreateBufferQueue(s32* out_consumer_binder_id, s32* out_producer_binder_id) { auto& nvmap = nvdrv->GetContainer().GetNvMapFile(); auto core = std::make_shared(); diff --git a/src/core/hle/service/nvnflinger/surface_flinger.h b/src/core/hle/service/nvnflinger/surface_flinger.h index 406281c83..386105f8e 100644 --- a/src/core/hle/service/nvnflinger/surface_flinger.h +++ b/src/core/hle/service/nvnflinger/surface_flinger.h @@ -44,10 +44,12 @@ public: void SetLayerVisibility(s32 consumer_binder_id, bool visible); void SetLayerBlending(s32 consumer_binder_id, LayerBlending blending); + void SetLayerIsOverlay(s32 consumer_binder_id, bool is_overlay); + + std::shared_ptr FindLayer(s32 consumer_binder_id); private: Display* FindDisplay(u64 display_id); - std::shared_ptr FindLayer(s32 consumer_binder_id); public: // TODO: these don't belong here diff --git a/src/core/hle/service/pctl/parental_control_service.cpp b/src/core/hle/service/pctl/parental_control_service.cpp index 0ba878799..42f38181d 100644 --- a/src/core/hle/service/pctl/parental_control_service.cpp +++ b/src/core/hle/service/pctl/parental_control_service.cpp @@ -36,6 +36,7 @@ IParentalControlService::IParentalControlService(Core::System& system_, Capabili {1016, nullptr, "ConfirmShowNewsPermission"}, {1017, D<&IParentalControlService::EndFreeCommunication>, "EndFreeCommunication"}, {1018, D<&IParentalControlService::IsFreeCommunicationAvailable>, "IsFreeCommunicationAvailable"}, + {1019, D<&IParentalControlService::ConfirmLaunchApplicationPermission>, "ConfirmLaunchApplicationPermission"}, {1031, D<&IParentalControlService::IsRestrictionEnabled>, "IsRestrictionEnabled"}, {1032, D<&IParentalControlService::GetSafetyLevel>, "GetSafetyLevel"}, {1033, nullptr, "SetSafetyLevel"}, diff --git a/src/core/hle/service/psc/ovln/ovln_types.h b/src/core/hle/service/psc/ovln/ovln_types.h index 343b05dcc..c3e61531f 100644 --- a/src/core/hle/service/psc/ovln/ovln_types.h +++ b/src/core/hle/service/psc/ovln/ovln_types.h @@ -18,4 +18,12 @@ union MessageFlags { }; static_assert(sizeof(MessageFlags) == 0x8, "MessageFlags has incorrect size"); +struct SourceName { + char name[0x16]; + + const char* GetString() const { + return name; + } +}; + } // namespace Service::PSC diff --git a/src/core/hle/service/psc/ovln/receiver.cpp b/src/core/hle/service/psc/ovln/receiver.cpp index 371ee5f83..2836ee611 100644 --- a/src/core/hle/service/psc/ovln/receiver.cpp +++ b/src/core/hle/service/psc/ovln/receiver.cpp @@ -5,32 +5,113 @@ #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/psc/ovln/receiver.h" + namespace Service::PSC { IReceiver::IReceiver(Core::System& system_) : ServiceFramework{system_, "IReceiver"}, service_context{system_, "IReceiver"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "AddSource"}, - {1, nullptr, "RemoveSource"}, + {0, D<&IReceiver::AddSource>, "AddSource"}, + {1, D<&IReceiver::RemoveSource>, "RemoveSource"}, {2, D<&IReceiver::GetReceiveEventHandle>, "GetReceiveEventHandle"}, - {3, nullptr, "Receive"}, - {4, nullptr, "ReceiveWithTick"}, + {3, D<&IReceiver::Receive>, "Receive"}, + {4, D<&IReceiver::ReceiveWithTick>, "ReceiveWithTick"}, }; // clang-format on RegisterHandlers(functions); - event = service_context.CreateEvent("IReceiver:Event"); + + receive_event = service_context.CreateEvent("IReceiver::ReceiveEvent"); } IReceiver::~IReceiver() { - service_context.CloseEvent(event); + service_context.CloseEvent(receive_event); } -Result IReceiver::GetReceiveEventHandle(OutCopyHandle out_event) { - LOG_DEBUG(Service_PSC, "called"); - *out_event = &event->GetReadableEvent(); +Result IReceiver::AddSource(SourceName source_name) { + const std::string name = source_name.GetString(); + LOG_INFO(Service_PSC, "called: source_name={}", name); + + // Add source if it doesn't already exist + if (message_sources.find(name) == message_sources.end()) { + message_sources[name] = {}; + } + R_SUCCEED(); } +Result IReceiver::RemoveSource(SourceName source_name) { + const std::string name = source_name.GetString(); + LOG_INFO(Service_PSC, "called: source_name={}", name); + + // Remove source if it exists + message_sources.erase(name); + + R_SUCCEED(); +} + +Result IReceiver::GetReceiveEventHandle(OutCopyHandle out_event) { + LOG_INFO(Service_PSC, "called"); + *out_event = &receive_event->GetReadableEvent(); + R_SUCCEED(); +} + +Result IReceiver::Receive(Out out_notification, Out out_flags) { + u64 tick; + return ReceiveWithTick(out_notification, out_flags, Out(&tick)); +} + +Result IReceiver::ReceiveWithTick(Out out_notification, + Out out_flags, Out out_tick) { + LOG_DEBUG(Service_PSC, "called"); + + // Find the message with the lowest ID across all sources + const std::string* target_source = nullptr; + size_t target_index = 0; + + for (const auto& [source_name, messages] : message_sources) { + if (!messages.empty()) { + if (target_source == nullptr) { + target_source = &source_name; + target_index = 0; + } + // Note: In the real implementation, we would track message IDs + // For now, just use FIFO order + } + } + + if (target_source != nullptr) { + auto& messages = message_sources[*target_source]; + *out_notification = messages[target_index].first; + *out_flags = messages[target_index].second; + *out_tick = 0; // TODO: Implement tick tracking + + // Remove the message + messages.erase(messages.begin() + target_index); + + // Clear event if no more messages + bool has_messages = false; + for (const auto& [_, msgs] : message_sources) { + if (!msgs.empty()) { + has_messages = true; + break; + } + } + if (!has_messages) { + receive_event->Clear(); + } + + R_SUCCEED(); + } + + // No messages available + *out_notification = {}; + *out_flags = {}; + *out_tick = 0; + + LOG_WARNING(Service_PSC, "No messages available"); + R_THROW(ResultUnknown); // TODO: Use proper OvlnResult::NoMessages when available +} + } // namespace Service::PSC diff --git a/src/core/hle/service/psc/ovln/receiver.h b/src/core/hle/service/psc/ovln/receiver.h index 9155decb7..b4e3fd3bd 100644 --- a/src/core/hle/service/psc/ovln/receiver.h +++ b/src/core/hle/service/psc/ovln/receiver.h @@ -3,12 +3,17 @@ #pragma once +#include +#include +#include +#include "core/hle/result.h" #include "core/hle/service/cmif_types.h" #include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/psc/ovln/ovln_types.h" #include "core/hle/service/service.h" + namespace Kernel { -class KEvent; class KReadableEvent; } // namespace Kernel @@ -20,10 +25,18 @@ public: ~IReceiver() override; private: + Result AddSource(SourceName source_name); + Result RemoveSource(SourceName source_name); Result GetReceiveEventHandle(OutCopyHandle out_event); + Result Receive(Out out_notification, Out out_flags); + Result ReceiveWithTick(Out out_notification, Out out_flags, + Out out_tick); KernelHelpers::ServiceContext service_context; - Kernel::KEvent* event; + Kernel::KEvent* receive_event; + + std::map>> + message_sources; }; } // namespace Service::PSC diff --git a/src/core/hle/service/set/settings_types.h b/src/core/hle/service/set/settings_types.h index 92c2948b0..efa764f7b 100644 --- a/src/core/hle/service/set/settings_types.h +++ b/src/core/hle/service/set/settings_types.h @@ -431,6 +431,15 @@ static_assert(sizeof(FirmwareVersionFormat) == 0x100, "FirmwareVersionFormat is static_assert(std::is_trivial_v, "FirmwareVersionFormat type must be trivially copyable."); +/// This is nn::settings::system::RebootlessSystemUpdateVersion +struct RebootlessSystemUpdateVersion { + u32 version; + std::array display_version; + INSERT_PADDING_BYTES(4); +}; +static_assert(sizeof(RebootlessSystemUpdateVersion) == 0x20, + "RebootlessSystemUpdateVersion is an invalid size"); + /// This is nn::settings::system::HomeMenuScheme struct HomeMenuScheme { u32 main; diff --git a/src/core/hle/service/set/system_settings_server.cpp b/src/core/hle/service/set/system_settings_server.cpp index eff511460..0f7254d8b 100644 --- a/src/core/hle/service/set/system_settings_server.cpp +++ b/src/core/hle/service/set/system_settings_server.cpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include "common/assert.h" @@ -239,7 +240,7 @@ ISystemSettingsServer::ISystemSettingsServer(Core::System& system_) {146, nullptr, "SetConsoleSixAxisSensorAngularVelocityTimeBias"}, {147, nullptr, "GetConsoleSixAxisSensorAngularAcceleration"}, {148, nullptr, "SetConsoleSixAxisSensorAngularAcceleration"}, - {149, nullptr, "GetRebootlessSystemUpdateVersion"}, + {149, D<&ISystemSettingsServer::GetRebootlessSystemUpdateVersion>, "GetRebootlessSystemUpdateVersion"}, {150, C<&ISystemSettingsServer::GetDeviceTimeZoneLocationUpdatedTime>, "GetDeviceTimeZoneLocationUpdatedTime"}, {151, C<&ISystemSettingsServer::SetDeviceTimeZoneLocationUpdatedTime>, "SetDeviceTimeZoneLocationUpdatedTime"}, {152, C<&ISystemSettingsServer::GetUserSystemClockAutomaticCorrectionUpdatedTime>, "GetUserSystemClockAutomaticCorrectionUpdatedTime"}, @@ -852,6 +853,17 @@ Result ISystemSettingsServer::SetQuestFlag(QuestFlag quest_flag) { R_SUCCEED(); } +Result ISystemSettingsServer::GetRebootlessSystemUpdateVersion( + Out out_version) { + LOG_WARNING(Service_SET, "(STUBBED) called"); + + out_version->version = 0; + std::memset(out_version->display_version.data(), 0, out_version->display_version.size()); + std::strcpy(out_version->display_version.data(), "0.0.0"); + + R_SUCCEED(); +} + Result ISystemSettingsServer::GetDeviceTimeZoneLocationName( Out out_name) { LOG_INFO(Service_SET, "called"); diff --git a/src/core/hle/service/set/system_settings_server.h b/src/core/hle/service/set/system_settings_server.h index 45ea2d8a8..8bbfa13ea 100644 --- a/src/core/hle/service/set/system_settings_server.h +++ b/src/core/hle/service/set/system_settings_server.h @@ -92,6 +92,7 @@ public: Result SetSpeakerAutoMuteFlag(bool force_mute_on_headphone_removed); Result GetQuestFlag(Out out_quest_flag); Result SetQuestFlag(QuestFlag quest_flag); + Result GetRebootlessSystemUpdateVersion(Out out_version); Result GetDeviceTimeZoneLocationName(Out out_name); Result SetDeviceTimeZoneLocationName(const Service::PSC::Time::LocationName& name); Result SetRegionCode(SystemRegionCode region_code); diff --git a/src/core/hle/service/vi/container.cpp b/src/core/hle/service/vi/container.cpp index 9074f4ae0..7494865d2 100644 --- a/src/core/hle/service/vi/container.cpp +++ b/src/core/hle/service/vi/container.cpp @@ -131,6 +131,49 @@ Result Container::SetLayerBlending(u64 layer_id, bool enabled) { R_SUCCEED(); } +Result Container::SetLayerZIndex(u64 layer_id, s32 z_index) { + std::scoped_lock lk{m_lock}; + + auto* const layer = m_layers.GetLayerById(layer_id); + R_UNLESS(layer != nullptr, VI::ResultNotFound); + + if (auto layer_ref = m_surface_flinger->FindLayer(layer->GetConsumerBinderId())) { + LOG_DEBUG(Service_VI, "called, SetLayerZIndex layer_id={} z={} (cid={})", layer_id, z_index, + layer->GetConsumerBinderId()); + layer_ref->z_index = z_index; + } else { + LOG_DEBUG(Service_VI, + "called, SetLayerZIndex failed to find layer for layer_id={} (cid={})", layer_id, + layer->GetConsumerBinderId()); + } + + R_SUCCEED(); +} + +Result Container::GetLayerZIndex(u64 layer_id, s32* out_z_index) { + std::scoped_lock lk{m_lock}; + + auto* const layer = m_layers.GetLayerById(layer_id); + R_UNLESS(layer != nullptr, VI::ResultNotFound); + + if (auto layer_ref = m_surface_flinger->FindLayer(layer->GetConsumerBinderId())) { + *out_z_index = layer_ref->z_index; + R_SUCCEED(); + } + + R_RETURN(VI::ResultNotFound); +} + +Result Container::SetLayerIsOverlay(u64 layer_id, bool is_overlay) { + std::scoped_lock lk{m_lock}; + + auto* const layer = m_layers.GetLayerById(layer_id); + R_UNLESS(layer != nullptr, VI::ResultNotFound); + + m_surface_flinger->SetLayerIsOverlay(layer->GetConsumerBinderId(), is_overlay); + R_SUCCEED(); +} + void Container::LinkVsyncEvent(u64 display_id, Event* event) { std::scoped_lock lk{m_lock}; m_conductor->LinkVsyncEvent(display_id, event); diff --git a/src/core/hle/service/vi/container.h b/src/core/hle/service/vi/container.h index 5eac4d77d..f00fd1759 100644 --- a/src/core/hle/service/vi/container.h +++ b/src/core/hle/service/vi/container.h @@ -62,6 +62,9 @@ public: Result SetLayerVisibility(u64 layer_id, bool visible); Result SetLayerBlending(u64 layer_id, bool enabled); + Result SetLayerZIndex(u64 layer_id, s32 z_index); + Result GetLayerZIndex(u64 layer_id, s32* out_z_index); + Result SetLayerIsOverlay(u64 layer_id, bool is_overlay); void LinkVsyncEvent(u64 display_id, Event* event); void UnlinkVsyncEvent(u64 display_id, Event* event); diff --git a/src/core/hle/service/vi/manager_display_service.cpp b/src/core/hle/service/vi/manager_display_service.cpp index 9f856282e..bdd524e03 100644 --- a/src/core/hle/service/vi/manager_display_service.cpp +++ b/src/core/hle/service/vi/manager_display_service.cpp @@ -116,6 +116,10 @@ Result IManagerDisplayService::SetLayerBlending(bool enabled, u64 layer_id) { R_RETURN(m_container->SetLayerBlending(layer_id, enabled)); } +Result IManagerDisplayService::SetLayerZIndex(s32 z_index, u64 layer_id) { + R_RETURN(m_container->SetLayerZIndex(layer_id, z_index)); +} + Result IManagerDisplayService::CreateManagedLayer(Out out_layer_id, u32 flags, u64 display_id, AppletResourceUserId aruid) { LOG_DEBUG(Service_VI, "called. flags={}, display={}, aruid={}", flags, display_id, aruid.pid); diff --git a/src/core/hle/service/vi/manager_display_service.h b/src/core/hle/service/vi/manager_display_service.h index b1bdf7f41..21fa39181 100644 --- a/src/core/hle/service/vi/manager_display_service.h +++ b/src/core/hle/service/vi/manager_display_service.h @@ -22,6 +22,7 @@ public: void DestroySharedLayerSession(Kernel::KProcess* owner_process); Result SetLayerBlending(bool enabled, u64 layer_id); + Result SetLayerZIndex(s32 z_index, u64 layer_id); public: Result CreateManagedLayer(Out out_layer_id, u32 flags, u64 display_id, From d18df2919ba7131bb8c5e1d1e2564e3bf5cb8272 Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 03:29:10 -0500 Subject: [PATCH 02/16] Remove Redundant yuzu_meta testing --- src/core/file_sys/registered_cache.cpp | 60 -------------------------- src/core/file_sys/registered_cache.h | 3 -- 2 files changed, 63 deletions(-) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 5a858d433..b6c61d715 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -373,12 +373,6 @@ std::optional RegisteredCache::GetNcaIDFromMetadata(u64 title_id, const auto res1 = CheckMapForContentRecord(citron_meta, title_id, type); if (res1) return res1; - const auto res2 = CheckMapForContentRecord(legacy_meta, title_id, type); - if (res2) { - LOG_INFO(Loader, "Found content {:016X} type {:02X} in legacy_meta", title_id, - static_cast(type)); - return res2; - } return CheckMapForContentRecord(meta, title_id, type); } @@ -467,7 +461,6 @@ void RegisteredCache::Refresh() { const auto ids = AccumulateFiles(); ProcessFiles(ids); AccumulateCitronMeta(); - AccumulateLegacyMeta(); } RegisteredCache::RegisteredCache(VirtualDir dir_, ContentProviderParsingFunction parsing_function) @@ -497,11 +490,6 @@ std::optional RegisteredCache::GetEntryVersion(u64 title_id) const { return citron_meta_iter->second.GetTitleVersion(); } - const auto legacy_meta_iter = legacy_meta.find(title_id); - if (legacy_meta_iter != legacy_meta.cend()) { - return legacy_meta_iter->second.GetTitleVersion(); - } - return std::nullopt; } @@ -539,14 +527,6 @@ void RegisteredCache::IterateAllMetadata( } } } - for (const auto& kv : legacy_meta) { - const auto& cnmt = kv.second; - for (const auto& rec : cnmt.GetContentRecords()) { - if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { - out.push_back(proc(cnmt, rec)); - } - } - } } std::vector RegisteredCache::ListEntriesFilter( @@ -778,15 +758,6 @@ bool RegisteredCache::RemoveExistingEntry(u64 title_id) const { } } - // If patch entries for any program exist in yuzu meta, remove them - for (u8 i = 0; i < 0x10; i++) { - const auto meta_dir = dir->CreateDirectoryRelative("yuzu_meta"); - const auto filename = GetCNMTName(TitleType::Update, title_id + i); - if (meta_dir->GetFile(filename)) { - removed_data |= meta_dir->DeleteFile(filename); - } - } - return removed_data; } @@ -1065,35 +1036,4 @@ std::vector ManualContentProvider::ListEntriesFilter( return out; } -void RegisteredCache::AccumulateLegacyMeta() { - LOG_INFO(Loader, "AccumulateLegacyMeta: Scanning directory '{}' for yuzu_meta", - dir->GetFullPath()); - - const auto meta_dir = dir->GetSubdirectory("yuzu_meta"); - if (meta_dir == nullptr) { - LOG_INFO(Loader, "AccumulateLegacyMeta: yuzu_meta directory not found in '{}'", - dir->GetFullPath()); - return; - } - LOG_INFO(Loader, "Accumulating legacy meta from yuzu_meta at '{}'", meta_dir->GetFullPath()); - - const auto files = meta_dir->GetFiles(); - LOG_INFO(Loader, "yuzu_meta contains {} files", files.size()); - - for (const auto& file : files) { - LOG_INFO(Loader, "Scanning file: name={} extension={}", file->GetName(), - file->GetExtension()); - if (file->GetExtension() != "cnmt") { - continue; - } - - CNMT cnmt(file); - LOG_INFO(Loader, "Loaded legacy CNMT: {:016X}", cnmt.GetTitleID()); - legacy_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt)); - } - - const auto subdirs = meta_dir->GetSubdirectories(); - LOG_INFO(Loader, "yuzu_meta contains {} subdirectories", subdirs.size()); -} - } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index 1d188178e..5b56c67c5 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -186,7 +186,6 @@ private: std::vector AccumulateFiles() const; void ProcessFiles(const std::vector& ids); void AccumulateCitronMeta(); - void AccumulateLegacyMeta(); std::optional GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const; VirtualFile GetFileAtID(NcaID id) const; VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& open_dir, std::string_view path) const; @@ -203,8 +202,6 @@ private: std::map meta; // maps tid -> meta for CNMT in citron_meta std::map citron_meta; - // maps tid -> meta for CNMT in yuzu_meta (legacy) - std::map legacy_meta; }; enum class ContentProviderUnionSlot { From 75dfdce858af65ee0edabf9cca6f723b9a5e8bd5 Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 06:37:06 -0500 Subject: [PATCH 03/16] Add more missing services --- src/core/hle/service/am/applet_manager.cpp | 46 +++++++++++++++++-- src/core/hle/service/am/applet_manager.h | 3 ++ .../am/frontend/applet_web_browser.cpp | 6 +++ .../am/frontend/applet_web_browser_types.h | 1 + .../hle/service/filesystem/filesystem.cpp | 14 +++--- src/core/hle/service/filesystem/filesystem.h | 5 +- .../hle/service/olsc/daemon_controller.cpp | 44 ++++++++++++++++-- src/core/hle/service/olsc/daemon_controller.h | 6 +++ .../olsc/remote_storage_controller.cpp | 20 +++++++- .../service/olsc/remote_storage_controller.h | 5 +- .../olsc/transfer_task_list_controller.cpp | 36 +++++++++++++-- .../olsc/transfer_task_list_controller.h | 7 +++ 12 files changed, 168 insertions(+), 25 deletions(-) diff --git a/src/core/hle/service/am/applet_manager.cpp b/src/core/hle/service/am/applet_manager.cpp index 2ed79880a..bd1b430eb 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -5,18 +5,22 @@ #include "common/uuid.h" #include "core/core.h" #include "core/core_timing.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/nca_metadata.h" #include "core/hle/service/acc/profile_manager.h" -#include "core/hle/service/am/process_creation.h" #include "core/hle/service/am/applet_data_broker.h" #include "core/hle/service/am/applet_manager.h" #include "core/hle/service/am/frontend/applet_cabinet.h" #include "core/hle/service/am/frontend/applet_controller.h" #include "core/hle/service/am/frontend/applet_mii_edit_types.h" #include "core/hle/service/am/frontend/applet_software_keyboard_types.h" +#include "core/hle/service/am/process_creation.h" #include "core/hle/service/am/service/storage.h" #include "core/hle/service/am/window_system.h" +#include "core/hle/service/filesystem/filesystem.h" #include "hid_core/hid_types.h" + namespace Service::AM { namespace { @@ -264,8 +268,10 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { m_cv.wait(lk, [&] { return m_pending_process != nullptr; }); if (true && m_window_system->GetOverlayDisplayApplet() == nullptr) { - if (auto overlay_process = CreateProcess(m_system, static_cast(AppletProgramId::OverlayDisplay), 0, 0)) { - auto overlay_applet = std::make_shared(m_system, std::move(overlay_process), false); + if (auto overlay_process = + CreateProcess(m_system, static_cast(AppletProgramId::OverlayDisplay), 0, 0)) { + auto overlay_applet = + std::make_shared(m_system, std::move(overlay_process), false); overlay_applet->program_id = static_cast(AppletProgramId::OverlayDisplay); overlay_applet->applet_id = AppletId::OverlayDisplay; overlay_applet->type = AppletType::OverlayApplet; @@ -275,7 +281,8 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { overlay_applet->home_button_long_pressed_blocked = false; m_window_system->TrackApplet(overlay_applet, false); overlay_applet->process->Run(); - LOG_INFO(Service_AM, "called, Overlay applet launched before application (initially hidden, watching home button)"); + LOG_INFO(Service_AM, "called, Overlay applet launched before application (initially " + "hidden, watching home button)"); } } @@ -291,6 +298,37 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { // Push UserChannel data from previous application if (params.launch_type == LaunchType::ApplicationInitiated) { applet->user_channel_launch_parameter.swap(m_system.GetUserChannel()); + + // Register game NCAs for QLaunch DLC support + m_manual_provider.ClearAllEntries(); + const auto title_id = params.program_id; + auto& system_provider = m_system.GetContentProviderUnion(); + + LOG_INFO(Service_AM, "QLaunch Support: Registering NCAs for title_id={:016X}", title_id); + + // Register Program NCA + auto game_nca = system_provider.GetEntry(title_id, FileSys::ContentRecordType::Program); + if (game_nca) { + m_manual_provider.AddEntry(FileSys::TitleType::Application, + FileSys::ContentRecordType::Program, title_id, + game_nca->GetBaseFile()); + LOG_DEBUG(Service_AM, "Registered Program NCA"); + } else { + LOG_WARNING(Service_AM, "Program NCA not found for title_id={:016X}", title_id); + } + + // Register Control NCA + auto control_nca = system_provider.GetEntry(title_id, FileSys::ContentRecordType::Control); + if (control_nca) { + m_manual_provider.AddEntry(FileSys::TitleType::Application, + FileSys::ContentRecordType::Control, title_id, + control_nca->GetBaseFile()); + LOG_DEBUG(Service_AM, "Registered Control NCA"); + } + + // Update the system's manual content provider slot to point to our populated provider + system_provider.SetSlot(FileSys::ContentProviderUnionSlot::FrontendManual, + &m_manual_provider); } // TODO: Read whether we need a preselected user from NACP? diff --git a/src/core/hle/service/am/applet_manager.h b/src/core/hle/service/am/applet_manager.h index 893de2eb7..406074e0e 100644 --- a/src/core/hle/service/am/applet_manager.h +++ b/src/core/hle/service/am/applet_manager.h @@ -7,6 +7,7 @@ #include #include +#include "core/file_sys/registered_cache.h" #include "core/hle/service/am/am_types.h" namespace Core { @@ -63,6 +64,8 @@ private: FrontendAppletParameters m_pending_parameters{}; std::unique_ptr m_pending_process{}; + + FileSys::ManualContentProvider m_manual_provider; }; } // namespace Service::AM diff --git a/src/core/hle/service/am/frontend/applet_web_browser.cpp b/src/core/hle/service/am/frontend/applet_web_browser.cpp index 0c86273a3..90c280ce4 100644 --- a/src/core/hle/service/am/frontend/applet_web_browser.cpp +++ b/src/core/hle/service/am/frontend/applet_web_browser.cpp @@ -280,6 +280,9 @@ void WebBrowser::Initialize() { case ShimKind::Lobby: InitializeLobby(); break; + case ShimKind::Unknown8: + LOG_WARNING(Service_AM, "(STUBBED) called, Unknown8 Applet is not implemented"); + break; default: ASSERT_MSG(false, "Invalid ShimKind={}", web_arg_header.shim_kind); break; @@ -317,6 +320,9 @@ void WebBrowser::Execute() { case ShimKind::Lobby: ExecuteLobby(); break; + case ShimKind::Unknown8: + WebBrowserExit(WebExitReason::EndButtonPressed); + break; default: ASSERT_MSG(false, "Invalid ShimKind={}", web_arg_header.shim_kind); WebBrowserExit(WebExitReason::EndButtonPressed); diff --git a/src/core/hle/service/am/frontend/applet_web_browser_types.h b/src/core/hle/service/am/frontend/applet_web_browser_types.h index 2f7c05c24..6ff8d4722 100644 --- a/src/core/hle/service/am/frontend/applet_web_browser_types.h +++ b/src/core/hle/service/am/frontend/applet_web_browser_types.h @@ -30,6 +30,7 @@ enum class ShimKind : u32 { Web = 5, Wifi = 6, Lobby = 7, + Unknown8 = 8, }; enum class WebExitReason : u32 { diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index fb50a2698..351ed1e34 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -29,7 +29,6 @@ #include "core/hle/service/filesystem/save_data_controller.h" #include "core/hle/service/server_manager.h" #include "core/loader/loader.h" - namespace Service::FileSystem { static FileSys::VirtualDir GetDirectoryRelativeWrapped(FileSys::VirtualDir base, @@ -226,7 +225,8 @@ Result VfsDirectoryServiceWrapper::RenameDirectory(const std::string& src_path_, // Different parent directories - need to move by copying then deleting. // Based on LibHac's approach: create dest, copy contents recursively, delete source. - LOG_DEBUG(Service_FS, "Moving directory across tree from \"{}\" to \"{}\"", src_path, dest_path); + LOG_DEBUG(Service_FS, "Moving directory across tree from \"{}\" to \"{}\"", src_path, + dest_path); // Create the destination directory auto dest_parent = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(dest_path)); @@ -429,7 +429,8 @@ std::shared_ptr FileSystemController::CreateSaveDataFa !Settings::values.global_custom_save_path.GetValue().empty()) { base_save_path_str = Settings::values.global_custom_save_path.GetValue(); - LOG_INFO(Service_FS, "Save Path: Using Global Custom Save Path as the base: {}", base_save_path_str); + LOG_INFO(Service_FS, "Save Path: Using Global Custom Save Path as the base: {}", + base_save_path_str); } else { base_save_path_str = Common::FS::GetCitronPathString(CitronPath::NANDDir); LOG_INFO(Service_FS, "Save Path: Using default NAND as the base."); @@ -440,10 +441,11 @@ std::shared_ptr FileSystemController::CreateSaveDataFa // 2. Check for Mirroring. if (Settings::values.mirrored_save_paths.count(program_id)) { LOG_INFO(Service_FS, - "Save Path: Mirroring detected for Program ID {:016X}. Syncing against the determined base directory.", + "Save Path: Mirroring detected for Program ID {:016X}. Syncing against the " + "determined base directory.", program_id); return std::make_shared(system, program_id, - std::move(base_directory)); + std::move(base_directory)); } // 3. Check for Per-Game Custom Path override. @@ -466,7 +468,6 @@ std::shared_ptr FileSystemController::CreateSaveDataFa LOG_INFO(Service_FS, "Save Path: No overrides found. Using the determined base directory."); return std::make_shared(system, program_id, std::move(base_directory)); - } Result FileSystemController::OpenSDMC(FileSys::VirtualDir* out_sdmc) const { @@ -616,7 +617,6 @@ FileSys::RegisteredCache* FileSystemController::GetSDMCContents() const { return sdmc_factory->GetSDMCContents(); } - FileSys::PlaceholderCache* FileSystemController::GetSystemNANDPlaceholder() const { LOG_TRACE(Service_FS, "Opening System NAND Placeholder"); diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index 6dc60254f..665d03ebc 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h @@ -92,7 +92,6 @@ public: FileSys::RegisteredCache* GetUserNANDContents() const; FileSys::RegisteredCache* GetSDMCContents() const; FileSys::RegisteredCache* GetGameCardContents() const; - FileSys::PlaceholderCache* GetSystemNANDPlaceholder() const; FileSys::PlaceholderCache* GetUserNANDPlaceholder() const; FileSys::PlaceholderCache* GetSDMCPlaceholder() const; @@ -122,7 +121,9 @@ public: void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true); // getter for main.cpp to trigger the sync between custom game paths for separate emulators - FileSys::SaveDataFactory& GetSaveDataFactory() { return *global_save_data_factory; } + FileSys::SaveDataFactory& GetSaveDataFactory() { + return *global_save_data_factory; + } void Reset(); diff --git a/src/core/hle/service/olsc/daemon_controller.cpp b/src/core/hle/service/olsc/daemon_controller.cpp index 7823780a8..cc6f8d75b 100644 --- a/src/core/hle/service/olsc/daemon_controller.cpp +++ b/src/core/hle/service/olsc/daemon_controller.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_serialization.h" @@ -12,14 +13,14 @@ IDaemonController::IDaemonController(Core::System& system_) static const FunctionInfo functions[] = { {0, D<&IDaemonController::GetAutoTransferEnabledForAccountAndApplication>, "GetAutoTransferEnabledForAccountAndApplication"}, {1, nullptr, "SetAutoTransferEnabledForAccountAndApplication"}, - {2, nullptr, "GetGlobalUploadEnabledForAccount"}, - {3, nullptr, "SetGlobalUploadEnabledForAccount"}, + {2, D<&IDaemonController::GetGlobalUploadEnabledForAccount>, "GetGlobalUploadEnabledForAccount"}, + {3, D<&IDaemonController::SetGlobalUploadEnabledForAccount>, "SetGlobalUploadEnabledForAccount"}, {4, nullptr, "TouchAccount"}, - {5, nullptr, "GetGlobalDownloadEnabledForAccount"}, - {6, nullptr, "SetGlobalDownloadEnabledForAccount"}, + {5, D<&IDaemonController::GetGlobalDownloadEnabledForAccount>, "GetGlobalDownloadEnabledForAccount"}, + {6, D<&IDaemonController::SetGlobalDownloadEnabledForAccount>, "SetGlobalDownloadEnabledForAccount"}, {10, nullptr, "GetForbiddenSaveDataIndication"}, {11, nullptr, "GetStopperObject"}, - {12, nullptr, "GetState"}, + {12, D<&IDaemonController::GetAutonomyTaskStatus>, "GetAutonomyTaskStatus"}, }; // clang-format on @@ -37,4 +38,37 @@ Result IDaemonController::GetAutoTransferEnabledForAccountAndApplication(Out out_is_enabled, + Common::UUID user_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, user_id={}", user_id.FormattedString()); + *out_is_enabled = false; + R_SUCCEED(); +} + +Result IDaemonController::SetGlobalUploadEnabledForAccount(bool is_enabled, Common::UUID user_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, is_enabled={} user_id={}", is_enabled, + user_id.FormattedString()); + R_SUCCEED(); +} + +Result IDaemonController::GetGlobalDownloadEnabledForAccount(Out out_is_enabled, + Common::UUID user_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, user_id={}", user_id.FormattedString()); + *out_is_enabled = false; + R_SUCCEED(); +} + +Result IDaemonController::SetGlobalDownloadEnabledForAccount(bool is_enabled, + Common::UUID user_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, is_enabled={} user_id={}", is_enabled, + user_id.FormattedString()); + R_SUCCEED(); +} + +Result IDaemonController::GetAutonomyTaskStatus(Out out_status, Common::UUID user_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, user_id={}", user_id.FormattedString()); + *out_status = 0; // Status: Idle + R_SUCCEED(); +} + } // namespace Service::OLSC diff --git a/src/core/hle/service/olsc/daemon_controller.h b/src/core/hle/service/olsc/daemon_controller.h index dfad7f52a..9d8b7db71 100644 --- a/src/core/hle/service/olsc/daemon_controller.h +++ b/src/core/hle/service/olsc/daemon_controller.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/uuid.h" @@ -15,6 +16,11 @@ public: private: Result GetAutoTransferEnabledForAccountAndApplication(Out out_is_enabled, Common::UUID user_id, u64 application_id); + Result GetGlobalUploadEnabledForAccount(Out out_is_enabled, Common::UUID user_id); + Result SetGlobalUploadEnabledForAccount(bool is_enabled, Common::UUID user_id); + Result GetGlobalDownloadEnabledForAccount(Out out_is_enabled, Common::UUID user_id); + Result SetGlobalDownloadEnabledForAccount(bool is_enabled, Common::UUID user_id); + Result GetAutonomyTaskStatus(Out out_status, Common::UUID user_id); }; } // namespace Service::OLSC diff --git a/src/core/hle/service/olsc/remote_storage_controller.cpp b/src/core/hle/service/olsc/remote_storage_controller.cpp index 181fff1bf..63aa9ede0 100644 --- a/src/core/hle/service/olsc/remote_storage_controller.cpp +++ b/src/core/hle/service/olsc/remote_storage_controller.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_serialization.h" @@ -21,11 +22,11 @@ IRemoteStorageController::IRemoteStorageController(Core::System& system_) {11, nullptr, "CreateDeleteDataTask"}, {12, nullptr, "DeleteSeriesInfo"}, {13, nullptr, "CreateRegisterNotificationTokenTask"}, - {14, nullptr, "UpdateSeriesInfo"}, + {14, D<&IRemoteStorageController::GetDataNewnessByApplicationId>, "GetDataNewnessByApplicationId"}, {15, nullptr, "RegisterUploadSaveDataTransferTaskForAutonomyRegistration"}, {16, nullptr, "CreateCleanupToDeleteSaveDataArchiveInfoTask"}, {17, nullptr, "ListDataInfo"}, - {18, nullptr, "GetDataInfo"}, + {18, D<&IRemoteStorageController::GetDataInfo>, "GetDataInfo"}, {19, nullptr, "Unknown19"}, {20, nullptr, "CreateSaveDataArchiveInfoCacheForSaveDataBackupUpdationTask"}, {21, nullptr, "ListSecondarySaves"}, @@ -33,6 +34,7 @@ IRemoteStorageController::IRemoteStorageController(Core::System& system_) {23, nullptr, "TouchSecondarySave"}, {24, nullptr, "GetSecondarySaveDataInfo"}, {25, nullptr, "RegisterDownloadSaveDataTransferTaskForAutonomyRegistration"}, + {27, D<&IRemoteStorageController::GetDataInfo>, "GetDataInfoV2"}, // [20.0.0+] {28, D<&IRemoteStorageController::Unknown28>, "Unknown28"}, // [20.2.0+] {900, nullptr, "Unknown900"}, {901, D<&IRemoteStorageController::Unknown901>, "Unknown901"}, // [20.2.0+] @@ -44,6 +46,20 @@ IRemoteStorageController::IRemoteStorageController(Core::System& system_) IRemoteStorageController::~IRemoteStorageController() = default; +Result IRemoteStorageController::GetDataNewnessByApplicationId(Out out_newness, + u64 application_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, application_id={:016X}", application_id); + *out_newness = 0; + R_SUCCEED(); +} + +Result IRemoteStorageController::GetDataInfo(Out> out_data, + u64 application_id) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, application_id={:016X}", application_id); + out_data->fill(0); + R_SUCCEED(); +} + Result IRemoteStorageController::GetSecondarySave(Out out_has_secondary_save, Out> out_unknown, u64 application_id) { diff --git a/src/core/hle/service/olsc/remote_storage_controller.h b/src/core/hle/service/olsc/remote_storage_controller.h index 59869322b..65c47dced 100644 --- a/src/core/hle/service/olsc/remote_storage_controller.h +++ b/src/core/hle/service/olsc/remote_storage_controller.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_types.h" @@ -12,9 +13,11 @@ public: ~IRemoteStorageController() override; private: + Result GetDataNewnessByApplicationId(Out out_newness, u64 application_id); + Result GetDataInfo(Out> out_data, u64 application_id); Result GetSecondarySave(Out out_has_secondary_save, Out> out_unknown, u64 application_id); - Result Unknown28(); // [20.2.0+] + Result Unknown28(); // [20.2.0+] Result Unknown901(); // [20.2.0+] }; diff --git a/src/core/hle/service/olsc/transfer_task_list_controller.cpp b/src/core/hle/service/olsc/transfer_task_list_controller.cpp index 8ea9a0f1e..4d1ef4644 100644 --- a/src/core/hle/service/olsc/transfer_task_list_controller.cpp +++ b/src/core/hle/service/olsc/transfer_task_list_controller.cpp @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/olsc/native_handle_holder.h" +#include "core/hle/service/olsc/remote_storage_controller.h" #include "core/hle/service/olsc/transfer_task_list_controller.h" namespace Service::OLSC { @@ -19,7 +21,7 @@ ITransferTaskListController::ITransferTaskListController(Core::System& system_) {5, D<&ITransferTaskListController::GetNativeHandleHolder>, "GetNativeHandleHolder"}, {6, nullptr, "Unknown6"}, {7, nullptr, "Unknown7"}, - {8, nullptr, "GetRemoteStorageController"}, + {8, D<&ITransferTaskListController::GetRemoteStorageController>, "GetRemoteStorageController"}, {9, D<&ITransferTaskListController::GetNativeHandleHolder>, "GetNativeHandleHolder2"}, {10, nullptr, "Unknown10"}, {11, nullptr, "Unknown11"}, @@ -31,12 +33,12 @@ ITransferTaskListController::ITransferTaskListController(Core::System& system_) {17, nullptr, "Unknown17"}, {18, nullptr, "Unknown18"}, {19, nullptr, "Unknown19"}, - {20, nullptr, "Unknown20"}, + {20, D<&ITransferTaskListController::Unknown20>, "Unknown20"}, {21, nullptr, "Unknown21"}, {22, nullptr, "Unknown22"}, {23, nullptr, "Unknown23"}, - {24, nullptr, "Unknown24"}, - {25, nullptr, "Unknown25"}, + {24, D<&ITransferTaskListController::GetCurrentTransferTaskInfo>, "GetCurrentTransferTaskInfo"}, + {25, D<&ITransferTaskListController::FindTransferTaskInfo>, "FindTransferTaskInfo"}, }; // clang-format on @@ -52,4 +54,30 @@ Result ITransferTaskListController::GetNativeHandleHolder( R_SUCCEED(); } +Result ITransferTaskListController::GetRemoteStorageController( + Out> out_controller) { + LOG_WARNING(Service_OLSC, "(STUBBED) called"); + *out_controller = std::make_shared(system); + R_SUCCEED(); +} + +Result ITransferTaskListController::Unknown20() { + LOG_WARNING(Service_OLSC, "(STUBBED) called"); + R_SUCCEED(); +} + +Result ITransferTaskListController::GetCurrentTransferTaskInfo(Out> out_info, + u8 unknown) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, unknown={:#x}", unknown); + out_info->fill(0); + R_SUCCEED(); +} + +Result ITransferTaskListController::FindTransferTaskInfo(Out> out_info, + InBuffer in) { + LOG_WARNING(Service_OLSC, "(STUBBED) called, in_size={}", in.size()); + out_info->fill(0); + R_SUCCEED(); +} + } // namespace Service::OLSC diff --git a/src/core/hle/service/olsc/transfer_task_list_controller.h b/src/core/hle/service/olsc/transfer_task_list_controller.h index f10a71375..23d555a27 100644 --- a/src/core/hle/service/olsc/transfer_task_list_controller.h +++ b/src/core/hle/service/olsc/transfer_task_list_controller.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/cmif_types.h" @@ -7,6 +8,7 @@ namespace Service::OLSC { class INativeHandleHolder; +class IRemoteStorageController; class ITransferTaskListController final : public ServiceFramework { public: @@ -15,6 +17,11 @@ public: private: Result GetNativeHandleHolder(Out> out_holder); + Result GetRemoteStorageController(Out> out_controller); + Result Unknown20(); + Result GetCurrentTransferTaskInfo(Out> out_info, u8 unknown); + Result FindTransferTaskInfo(Out> out_info, + InBuffer in); }; } // namespace Service::OLSC From c5ec4bd7f964bf1cc4077b99c428dd20561dfc1d Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 08:29:10 -0500 Subject: [PATCH 04/16] feat(hotkeys): Introduce Hotkey_Profile_Manager --- src/citron/CMakeLists.txt | 2 + src/citron/configuration/configure_dialog.cpp | 183 ++++++----- .../configuration/configure_hotkeys.cpp | 305 ++++++++++++++---- src/citron/configuration/configure_hotkeys.h | 28 +- src/citron/configuration/configure_hotkeys.ui | 82 ++++- src/citron/hotkey_profile_manager.cpp | 239 ++++++++++++++ src/citron/hotkey_profile_manager.h | 69 ++++ 7 files changed, 760 insertions(+), 148 deletions(-) create mode 100644 src/citron/hotkey_profile_manager.cpp create mode 100644 src/citron/hotkey_profile_manager.h diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index cb028c618..da419989d 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -178,6 +178,8 @@ add_executable(citron game_list_worker.h hotkeys.cpp hotkeys.h + hotkey_profile_manager.cpp + hotkey_profile_manager.h install_dialog.cpp install_dialog.h loading_screen.cpp diff --git a/src/citron/configuration/configure_dialog.cpp b/src/citron/configuration/configure_dialog.cpp index fe8a679b8..e1f425d6b 100644 --- a/src/citron/configuration/configure_dialog.cpp +++ b/src/citron/configuration/configure_dialog.cpp @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "citron/configuration/configure_dialog.h" #include #include #include @@ -20,17 +19,12 @@ #include #include #include -#include "common/logging/log.h" -#include "common/settings.h" -#include "common/settings_enums.h" -#include "core/core.h" -#include "ui_configure.h" -#include "vk_device_info.h" #include "citron/configuration/configuration_shared.h" #include "citron/configuration/configure_applets.h" #include "citron/configuration/configure_audio.h" #include "citron/configuration/configure_cpu.h" #include "citron/configuration/configure_debug_tab.h" +#include "citron/configuration/configure_dialog.h" #include "citron/configuration/configure_filesystem.h" #include "citron/configuration/configure_general.h" #include "citron/configuration/configure_graphics.h" @@ -44,12 +38,18 @@ #include "citron/configuration/configure_ui.h" #include "citron/configuration/configure_web.h" #include "citron/configuration/style_animation_event_filter.h" -#include "citron/util/rainbow_style.h" #include "citron/game_list.h" #include "citron/hotkeys.h" #include "citron/main.h" #include "citron/theme.h" #include "citron/uisettings.h" +#include "citron/util/rainbow_style.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/settings_enums.h" +#include "core/core.h" +#include "ui_configure.h" +#include "vk_device_info.h" static QScrollArea* CreateScrollArea(QWidget* widget) { auto* scroll_area = new QScrollArea(); @@ -95,7 +95,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui_tab->UpdateScreenshotInfo(ratio, setup); }, nullptr, *builder, this)}, - hotkeys_tab{std::make_unique(system_.HIDCore(), this)}, + hotkeys_tab{std::make_unique(registry, system_.HIDCore(), this)}, input_tab{std::make_unique(system_, this)}, network_tab{std::make_unique(system_, this)}, profile_tab{std::make_unique(system_, this)}, @@ -103,8 +103,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, web_tab{std::make_unique(this)} { if (auto* main_window = qobject_cast(parent)) { - connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh, - main_window, &GMainWindow::RefreshGameList); + connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh, main_window, + &GMainWindow::RefreshGameList); } Settings::SetConfiguringGlobal(true); @@ -115,7 +115,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); setWindowModality(Qt::NonModal); } else { - setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | + Qt::WindowCloseButtonHint); setWindowModality(Qt::WindowModal); } @@ -180,18 +181,21 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui->stackedWidget->addWidget(CreateScrollArea(applets_tab.get())); ui->stackedWidget->addWidget(CreateScrollArea(debug_tab_tab.get())); - connect(tab_button_group.get(), qOverload(&QButtonGroup::idClicked), this, &ConfigureDialog::AnimateTabSwitch); + connect(tab_button_group.get(), qOverload(&QButtonGroup::idClicked), this, + &ConfigureDialog::AnimateTabSwitch); connect(ui_tab.get(), &ConfigureUi::themeChanged, this, &ConfigureDialog::UpdateTheme); - connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this, &ConfigureDialog::SetUIPositioning); + connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this, + &ConfigureDialog::SetUIPositioning); web_tab->SetWebServiceConfigEnabled(enable_web_config); - hotkeys_tab->Populate(registry); + hotkeys_tab->Populate(); input_tab->Initialize(input_subsystem); general_tab->SetResetCallback([&] { this->close(); }); SetConfiguration(); connect(ui_tab.get(), &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged); if (system.IsPoweredOn()) { if (auto* apply_button = ui->buttonBox->button(QDialogButtonBox::Apply)) { - connect(apply_button, &QAbstractButton::clicked, this, &ConfigureDialog::HandleApplyButtonClicked); + connect(apply_button, &QAbstractButton::clicked, this, + &ConfigureDialog::HandleApplyButtonClicked); } } ui->stackedWidget->setCurrentIndex(0); @@ -219,10 +223,12 @@ void ConfigureDialog::UpdateTheme() { const QString d_txt = is_dark ? QStringLiteral("#8d8d8d") : QStringLiteral("#a0a0a0"); // Use dark shadow on light backgrounds, light shadow on dark backgrounds - const QString shadow_color = is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)"); + const QString shadow_color = + is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)"); static QString cached_template; - if (cached_template.isEmpty()) cached_template = property("templateStyleSheet").toString(); + if (cached_template.isEmpty()) + cached_template = property("templateStyleSheet").toString(); QString style_sheet = cached_template; style_sheet.replace(QStringLiteral("%%ACCENT_COLOR%%"), accent); @@ -237,10 +243,10 @@ void ConfigureDialog::UpdateTheme() { style_sheet.replace(QStringLiteral("%%FOCUS_BG_COLOR%%"), f_bg); style_sheet.replace(QStringLiteral("%%DISABLED_TEXT_COLOR%%"), d_txt); - style_sheet += QStringLiteral( - "QSlider::handle:horizontal { background-color: %1; }" - "QCheckBox::indicator:checked { background-color: %1; border-color: %1; }" - ).arg(accent); + style_sheet += + QStringLiteral("QSlider::handle:horizontal { background-color: %1; }" + "QCheckBox::indicator:checked { background-color: %1; border-color: %1; }") + .arg(accent); setStyleSheet(style_sheet); @@ -250,33 +256,37 @@ void ConfigureDialog::UpdateTheme() { cpu_tab->SetTemplateStyleSheet(style_sheet); graphics_advanced_tab->SetTemplateStyleSheet(style_sheet); - QString sidebar_css = QStringLiteral( - "QPushButton.tabButton { " + QString sidebar_css = + QStringLiteral( + "QPushButton.tabButton { " "background-color: %1; " "color: %2; " "border: 2px solid transparent; " - "}" - "QPushButton.tabButton:checked { " - "color: %4; " // Use main text color instead of dimmed color for checked state + "}" + "QPushButton.tabButton:checked { " + "color: %4; " // Use main text color instead of dimmed color for checked state "border: 2px solid %3; " - "}" - "QPushButton.tabButton:hover { " + "}" + "QPushButton.tabButton:hover { " "border: 2px solid %3; " - "}" - "QPushButton.tabButton:pressed { " + "}" + "QPushButton.tabButton:pressed { " "background-color: %3; " "color: #ffffff; " - "}" - ).arg(b_bg, d_txt, accent, txt); + "}") + .arg(b_bg, d_txt, accent, txt); - if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(sidebar_css); - if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css); + if (ui->topButtonWidget) + ui->topButtonWidget->setStyleSheet(sidebar_css); + if (ui->horizontalNavWidget) + ui->horizontalNavWidget->setStyleSheet(sidebar_css); if (is_rainbow) { if (!rainbow_timer) { rainbow_timer = new QTimer(this); connect(rainbow_timer, &QTimer::timeout, this, [this, b_bg, d_txt, txt, shadow_color] { - if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) { + if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() || + !this->isActiveWindow()) { return; } @@ -288,43 +298,52 @@ void ConfigureDialog::UpdateTheme() { const QString hue_light = current_color.lighter(125).name(); const QString hue_dark = current_color.darker(150).name(); - QString rainbow_sidebar_css = QStringLiteral( - "QPushButton.tabButton { " - "background-color: %1; " - "color: %2; " - "border: 2px solid transparent; " - "}" - "QPushButton.tabButton:checked { " - "color: %4; " // Use main text color for visibility - "border: 2px solid %3; " - "}" - "QPushButton.tabButton:hover { " - "border: 2px solid %3; " - "}" - "QPushButton.tabButton:pressed { " - "background-color: %3; " - "color: #ffffff; " - "}" - ).arg(b_bg, d_txt, hue_hex, txt); + QString rainbow_sidebar_css = + QStringLiteral("QPushButton.tabButton { " + "background-color: %1; " + "color: %2; " + "border: 2px solid transparent; " + "}" + "QPushButton.tabButton:checked { " + "color: %4; " // Use main text color for visibility + "border: 2px solid %3; " + "}" + "QPushButton.tabButton:hover { " + "border: 2px solid %3; " + "}" + "QPushButton.tabButton:pressed { " + "background-color: %3; " + "color: #ffffff; " + "}") + .arg(b_bg, d_txt, hue_hex, txt); - if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css); - if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css); + if (ui->topButtonWidget) + ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css); + if (ui->horizontalNavWidget) + ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css); // Tab Content Area - if (current_index == input_tab_index) return; + if (current_index == input_tab_index) + return; QWidget* currentContainer = ui->stackedWidget->currentWidget(); if (currentContainer) { - QString tab_css = QStringLiteral( - "QCheckBox::indicator:checked, QRadioButton::indicator:checked { background-color: %1; border: 1px solid %1; }" - "QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }" - "QSlider::handle:horizontal { background-color: %1; border: 1px solid %1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }" - "QPushButton, QToolButton { background-color: transparent; color: %4; border: 2px solid %1; border-radius: 4px; padding: 5px; }" - "QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }" - "QPushButton:pressed, QToolButton:pressed { background-color: %3; color: #ffffff; border-color: %3; }" - ).arg(hue_hex, hue_light, hue_dark, txt); + QString tab_css = + QStringLiteral( + "QCheckBox::indicator:checked, QRadioButton::indicator:checked { " + "background-color: %1; border: 1px solid %1; }" + "QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }" + "QSlider::handle:horizontal { background-color: %1; border: 1px solid " + "%1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }" + "QPushButton, QToolButton { background-color: transparent; color: %4; " + "border: 2px solid %1; border-radius: 4px; padding: 5px; }" + "QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }" + "QPushButton:pressed, QToolButton:pressed { background-color: %3; " + "color: #ffffff; border-color: %3; }") + .arg(hue_hex, hue_light, hue_dark, txt); currentContainer->setStyleSheet(tab_css); - if (ui->buttonBox) ui->buttonBox->setStyleSheet(tab_css); + if (ui->buttonBox) + ui->buttonBox->setStyleSheet(tab_css); } }); } @@ -334,7 +353,8 @@ void ConfigureDialog::UpdateTheme() { if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) { rainbow_timer->stop(); - if (ui->buttonBox) ui->buttonBox->setStyleSheet({}); + if (ui->buttonBox) + ui->buttonBox->setStyleSheet({}); for (int i = 0; i < ui->stackedWidget->count(); ++i) { if (auto* w = ui->stackedWidget->widget(i)) { w->setStyleSheet({}); @@ -370,7 +390,8 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) { if (!tab_buttons.empty()) { const int button_height = tab_buttons[0]->sizeHint().height(); - const int margins = h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom(); + const int margins = + h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom(); // The scroll area frame adds a few pixels, this accounts for it. const int fixed_height = button_height + margins + 4; ui->horizontalNavScrollArea->setMaximumHeight(fixed_height); @@ -417,7 +438,7 @@ void ConfigureDialog::ApplyConfiguration() { profile_tab->ApplyConfiguration(); filesystem_tab->ApplyConfiguration(); input_tab->ApplyConfiguration(); - hotkeys_tab->ApplyConfiguration(registry); + hotkeys_tab->ApplyConfiguration(); cpu_tab->ApplyConfiguration(); graphics_tab->ApplyConfiguration(); graphics_advanced_tab->ApplyConfiguration(); @@ -501,7 +522,8 @@ void ConfigureDialog::AnimateTabSwitch(int id) { anim_new_opacity->setDuration(duration); anim_new_opacity->setEasingCurve(QEasingCurve::InQuad); - auto* button_opacity_effect = qobject_cast(ui->buttonBox->graphicsEffect()); + auto* button_opacity_effect = + qobject_cast(ui->buttonBox->graphicsEffect()); if (!button_opacity_effect) { button_opacity_effect = new QGraphicsOpacityEffect(ui->buttonBox); ui->buttonBox->setGraphicsEffect(button_opacity_effect); @@ -529,18 +551,19 @@ void ConfigureDialog::AnimateTabSwitch(int id) { animation_group->addAnimation(anim_new_opacity); animation_group->addAnimation(button_anim_sequence); - connect(animation_group, &QAbstractAnimation::finished, this, [this, current_widget, next_widget, id]() { - ui->stackedWidget->setCurrentIndex(id); + connect(animation_group, &QAbstractAnimation::finished, this, + [this, current_widget, next_widget, id]() { + ui->stackedWidget->setCurrentIndex(id); - next_widget->setGraphicsEffect(nullptr); - current_widget->hide(); - current_widget->move(0, 0); + next_widget->setGraphicsEffect(nullptr); + current_widget->hide(); + current_widget->move(0, 0); - m_is_tab_animating = false; // Reset the flag - for (auto button : tab_button_group->buttons()) { - button->setEnabled(true); - } - }); + m_is_tab_animating = false; // Reset the flag + for (auto button : tab_button_group->buttons()) { + button->setEnabled(true); + } + }); m_is_tab_animating = true; // Set the flag for (auto button : tab_button_group->buttons()) { diff --git a/src/citron/configuration/configure_hotkeys.cpp b/src/citron/configuration/configure_hotkeys.cpp index ac08b7527..1a18a099f 100644 --- a/src/citron/configuration/configure_hotkeys.cpp +++ b/src/citron/configuration/configure_hotkeys.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-FileCopyrightText: 2026 Citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include #include #include @@ -9,19 +12,21 @@ #include "hid_core/frontend/emulated_controller.h" #include "hid_core/hid_core.h" -#include "frontend_common/config.h" -#include "ui_configure_hotkeys.h" #include "citron/configuration/configure_hotkeys.h" #include "citron/hotkeys.h" #include "citron/uisettings.h" #include "citron/util/sequence_dialog/sequence_dialog.h" +#include "frontend_common/config.h" +#include "ui_configure_hotkeys.h" constexpr int name_column = 0; constexpr int hotkey_column = 1; constexpr int controller_column = 2; -ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent) - : QWidget(parent), ui(std::make_unique()), +ConfigureHotkeys::ConfigureHotkeys(HotkeyRegistry& registry_, Core::HID::HIDCore& hid_core, + QWidget* parent) + : QWidget(parent), ui(std::make_unique()), registry(registry_), + controller(new Core::HID::EmulatedController(Core::HID::NpadIdType::Player1)), timeout_timer(std::make_unique()), poll_timer(std::make_unique()) { ui->setupUi(this); setFocusPolicy(Qt::ClickFocus); @@ -36,14 +41,28 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent ui->hotkey_list->setModel(model); ui->hotkey_list->header()->setStretchLastSection(false); - ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::ResizeMode::Stretch); - ui->hotkey_list->header()->setMinimumSectionSize(150); + ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive); + ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive); + ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch); + ui->hotkey_list->header()->setMinimumSectionSize(70); connect(ui->button_restore_defaults, &QPushButton::clicked, this, &ConfigureHotkeys::RestoreDefaults); connect(ui->button_clear_all, &QPushButton::clicked, this, &ConfigureHotkeys::ClearAll); - controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + // Profile Management Connections + connect(ui->button_new_profile, &QPushButton::clicked, this, + &ConfigureHotkeys::OnCreateProfile); + connect(ui->button_delete_profile, &QPushButton::clicked, this, + &ConfigureHotkeys::OnDeleteProfile); + connect(ui->button_rename_profile, &QPushButton::clicked, this, + &ConfigureHotkeys::OnRenameProfile); + connect(ui->button_import_profile, &QPushButton::clicked, this, + &ConfigureHotkeys::OnImportProfile); + connect(ui->button_export_profile, &QPushButton::clicked, this, + &ConfigureHotkeys::OnExportProfile); + connect(ui->combo_box_profile, qOverload(&QComboBox::currentIndexChanged), this, + &ConfigureHotkeys::OnProfileChanged); connect(timeout_timer.get(), &QTimer::timeout, [this] { const bool is_button_pressed = pressed_buttons != Core::HID::NpadButton::None || @@ -53,8 +72,8 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent connect(poll_timer.get(), &QTimer::timeout, [this] { pressed_buttons |= controller->GetNpadButtons().raw; - pressed_home_button |= this->controller->GetHomeButtons().home != 0; - pressed_capture_button |= this->controller->GetCaptureButtons().capture != 0; + pressed_home_button |= controller->GetHomeButtons().home != 0; + pressed_capture_button |= controller->GetCaptureButtons().capture != 0; if (pressed_buttons != Core::HID::NpadButton::None || pressed_home_button || pressed_capture_button) { const QString button_name = @@ -64,38 +83,177 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent model->setData(button_model_index, button_name); } }); - RetranslateUI(); + + ui->hotkey_list->setContextMenuPolicy(Qt::CustomContextMenu); + + // Populate profile list first + UpdateProfileList(); } ConfigureHotkeys::~ConfigureHotkeys() = default; -void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) { - for (const auto& group : registry.hotkey_groups) { - QString parent_item_data = QString::fromStdString(group.first); - auto* parent_item = - new QStandardItem(QCoreApplication::translate("Hotkeys", qPrintable(parent_item_data))); - parent_item->setEditable(false); - parent_item->setData(parent_item_data); - for (const auto& hotkey : group.second) { - QString hotkey_action_data = QString::fromStdString(hotkey.first); - auto* action = new QStandardItem( - QCoreApplication::translate("Hotkeys", qPrintable(hotkey_action_data))); - auto* keyseq = - new QStandardItem(hotkey.second.keyseq.toString(QKeySequence::NativeText)); - auto* controller_keyseq = - new QStandardItem(QString::fromStdString(hotkey.second.controller_keyseq)); - action->setEditable(false); - action->setData(hotkey_action_data); - keyseq->setEditable(false); - controller_keyseq->setEditable(false); - parent_item->appendRow({action, keyseq, controller_keyseq}); - } - model->appendRow(parent_item); +void ConfigureHotkeys::Populate() { + const auto& profiles = profile_manager.GetProfiles(); + const auto& current_profile_name = profiles.current_profile; + + // Use default if current profile missing (safety) + std::vector current_shortcuts; + if (profiles.profiles.count(current_profile_name)) { + current_shortcuts = profiles.profiles.at(current_profile_name); + } else if (profiles.profiles.count("Default")) { + current_shortcuts = profiles.profiles.at("Default"); } + // Map overrides for easy lookup: Key = Group + Name + std::map, Hotkey::BackendShortcut> overrides; + for (const auto& s : current_shortcuts) { + overrides[{s.group, s.name}] = s; + } + + model->clear(); + model->setColumnCount(3); + model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")}); + + for (const auto& [group_name, group_map] : registry.hotkey_groups) { + auto* parent_item = new QStandardItem( + QCoreApplication::translate("Hotkeys", qPrintable(QString::fromStdString(group_name)))); + parent_item->setEditable(false); + parent_item->setData(QString::fromStdString(group_name)); + model->appendRow(parent_item); + + for (const auto& [action_name, hotkey] : group_map) { + // Determine values (Registry Default vs Profile Override) + QString keyseq_str = hotkey.keyseq.toString(QKeySequence::NativeText); + QString controller_keyseq_str = QString::fromStdString(hotkey.controller_keyseq); + + if (overrides.count({group_name, action_name})) { + const auto& overridden = overrides.at({group_name, action_name}); + keyseq_str = QKeySequence(QString::fromStdString(overridden.shortcut.keyseq)) + .toString(QKeySequence::NativeText); + controller_keyseq_str = + QString::fromStdString(overridden.shortcut.controller_keyseq); + } + + auto* action_item = new QStandardItem(QCoreApplication::translate( + "Hotkeys", qPrintable(QString::fromStdString(action_name)))); + action_item->setEditable(false); + action_item->setData(QString::fromStdString(action_name)); + + auto* keyseq_item = new QStandardItem(keyseq_str); + // Store raw keyseq string logic? + // The system likely expects QKeySequence string format. + keyseq_item->setData(keyseq_str, Qt::UserRole); + keyseq_item->setEditable(false); + + auto* controller_item = new QStandardItem(controller_keyseq_str); + controller_item->setEditable(false); + + parent_item->appendRow({action_item, keyseq_item, controller_item}); + } + + if (group_name == "General" || group_name == "Main Window") { + ui->hotkey_list->expand(parent_item->index()); + } + } ui->hotkey_list->expandAll(); - ui->hotkey_list->resizeColumnToContents(hotkey_column); - ui->hotkey_list->resizeColumnToContents(controller_column); + + // Re-apply column sizing after model reset + ui->hotkey_list->header()->setStretchLastSection(false); + ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive); + ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive); + ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch); + ui->hotkey_list->header()->setMinimumSectionSize(70); + + ui->hotkey_list->setColumnWidth(name_column, 432); + ui->hotkey_list->setColumnWidth(hotkey_column, 240); + + // Enforce fixed width for Restore Defaults button to prevent smudging + ui->button_restore_defaults->setFixedWidth(143); +} + +void ConfigureHotkeys::UpdateProfileList() { + const QSignalBlocker blocker(ui->combo_box_profile); + ui->combo_box_profile->clear(); + + const auto& profiles = profile_manager.GetProfiles(); + for (const auto& [name, val] : profiles.profiles) { + ui->combo_box_profile->addItem(QString::fromStdString(name)); + } + + ui->combo_box_profile->setCurrentText(QString::fromStdString(profiles.current_profile)); + Populate(); +} + +void ConfigureHotkeys::OnCreateProfile() { + bool ok; + QString text = QInputDialog::getText(this, tr("Create Profile"), tr("Profile Name:"), + QLineEdit::Normal, QString(), &ok); + if (ok && !text.isEmpty()) { + if (profile_manager.CreateProfile(text.toStdString())) { + // New profile is empty. Fill with current defaults or copy current? + // "Defaults" logic usually implies defaults. + UpdateProfileList(); + } else { + QMessageBox::warning(this, tr("Error"), tr("Failed to create profile.")); + } + } +} + +void ConfigureHotkeys::OnDeleteProfile() { + if (QMessageBox::question(this, tr("Delete Profile"), + tr("Are you sure you want to delete this profile?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (profile_manager.DeleteProfile(ui->combo_box_profile->currentText().toStdString())) { + UpdateProfileList(); + } else { + QMessageBox::warning(this, tr("Error"), tr("Failed to delete profile.")); + } + } +} + +void ConfigureHotkeys::OnRenameProfile() { + bool ok; + QString current_name = ui->combo_box_profile->currentText(); + QString text = QInputDialog::getText(this, tr("Rename Profile"), tr("New Name:"), + QLineEdit::Normal, current_name, &ok); + if (ok && !text.isEmpty()) { + if (profile_manager.RenameProfile(current_name.toStdString(), text.toStdString())) { + UpdateProfileList(); + } else { + QMessageBox::warning(this, tr("Error"), tr("Failed to rename profile.")); + } + } +} + +void ConfigureHotkeys::OnImportProfile() { + QString fileName = QFileDialog::getOpenFileName(this, tr("Import Profile"), QString(), + tr("JSON Files (*.json)")); + if (!fileName.isEmpty()) { + if (profile_manager.ImportProfile(fileName.toStdString())) { + UpdateProfileList(); + } else { + QMessageBox::warning(this, tr("Error"), tr("Failed to import profile.")); + } + } +} + +void ConfigureHotkeys::OnExportProfile() { + QString current = ui->combo_box_profile->currentText(); + QString fileName = QFileDialog::getSaveFileName( + this, tr("Export Profile"), current + QStringLiteral(".json"), tr("JSON Files (*.json)")); + if (!fileName.isEmpty()) { + if (!profile_manager.ExportProfile(current.toStdString(), fileName.toStdString())) { + QMessageBox::warning(this, tr("Error"), tr("Failed to export profile.")); + } + } +} + +void ConfigureHotkeys::OnProfileChanged(int index) { + if (index == -1) + return; + const std::string name = ui->combo_box_profile->currentText().toStdString(); + profile_manager.SetCurrentProfile(name); + Populate(); } void ConfigureHotkeys::changeEvent(QEvent* event) { @@ -108,6 +266,7 @@ void ConfigureHotkeys::changeEvent(QEvent* event) { void ConfigureHotkeys::RetranslateUI() { ui->retranslateUi(this); + ui->label_profile->setText(tr("Hotkey Profile:")); model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")}); for (int key_id = 0; key_id < model->rowCount(); key_id++) { @@ -307,28 +466,67 @@ std::pair ConfigureHotkeys::IsUsedControllerKey(const QString& ke return std::make_pair(false, QString()); } -void ConfigureHotkeys::ApplyConfiguration(HotkeyRegistry& registry) { - for (int key_id = 0; key_id < model->rowCount(); key_id++) { - const QStandardItem* parent = model->item(key_id, 0); - for (int key_column_id = 0; key_column_id < parent->rowCount(); key_column_id++) { - const QStandardItem* action = parent->child(key_column_id, name_column); - const QStandardItem* keyseq = parent->child(key_column_id, hotkey_column); - const QStandardItem* controller_keyseq = - parent->child(key_column_id, controller_column); - for (auto& [group, sub_actions] : registry.hotkey_groups) { - if (group != parent->data().toString().toStdString()) - continue; - for (auto& [action_name, hotkey] : sub_actions) { - if (action_name != action->data().toString().toStdString()) - continue; - hotkey.keyseq = QKeySequence(keyseq->text()); - hotkey.controller_keyseq = controller_keyseq->text().toStdString(); +void ConfigureHotkeys::ApplyConfiguration() { + // 1. Update the runtime UISettings (Registry) + // We iterate the model and match against UISettings::values.shortcuts + const auto& children = model->invisibleRootItem(); + for (int group_row = 0; group_row < children->rowCount(); group_row++) { + const auto& group_item = children->child(group_row); + for (int row = 0; row < group_item->rowCount(); row++) { + const auto& action_item = group_item->child(row, name_column); + const auto& keyseq_item = group_item->child(row, hotkey_column); + const auto& controller_item = group_item->child(row, controller_column); + + const std::string group_name = group_item->data().toString().toStdString(); + const std::string action_name = action_item->data().toString().toStdString(); + + // Update UISettings (Runtime) + for (auto& s : UISettings::values.shortcuts) { + if (s.group == group_name && s.name == action_name) { + s.shortcut.keyseq = keyseq_item->text().toStdString(); + s.shortcut.controller_keyseq = controller_item->text().toStdString(); } } } } - registry.SaveHotkeys(); + // 2. Update the ProfileManager (Storage) + const std::string current_profile_name = profile_manager.GetProfiles().current_profile; + // We need to modify the profile in the manager. GetProfiles() returns const ref. + // We need a method to UpdateProfile or we need to cast away const (bad) or rely on reference if + // GetProfiles wasn't const. The previous implementation of GetProfiles was const. + // ProfileManager needs a method `UpdateCurrentProfile(vector)`? + // Or we can just use the internal map if we were friends. + // Reconstructing BackendShortcuts from UI + std::vector new_shortcuts; + for (int group_row = 0; group_row < children->rowCount(); group_row++) { + const auto& group_item = children->child(group_row); + for (int row = 0; row < group_item->rowCount(); row++) { + const auto& action_item = group_item->child(row, name_column); + const auto& keyseq_item = group_item->child(row, hotkey_column); + const auto& controller_item = group_item->child(row, controller_column); + + Hotkey::BackendShortcut s; + s.group = group_item->data().toString().toStdString(); + s.name = action_item->data().toString().toStdString(); + s.shortcut.keyseq = keyseq_item->text().toStdString(); + s.shortcut.controller_keyseq = controller_item->text().toStdString(); + // Context/Repeat need to be preserved from UserRole data + // For now, let's grab from UISettings since we just updated it or match it. + + for (const auto& original : UISettings::values.shortcuts) { + if (original.group == s.group && original.name == s.name) { + s.shortcut.context = original.shortcut.context; + s.shortcut.repeat = original.shortcut.repeat; + break; + } + } + new_shortcuts.push_back(s); + } + } + + profile_manager.SetProfileShortcuts(current_profile_name, new_shortcuts); + profile_manager.Save(); } void ConfigureHotkeys::RestoreDefaults() { @@ -339,11 +537,10 @@ void ConfigureHotkeys::RestoreDefaults() { QStandardItem* parent = model->item(group_row, 0); for (int child_row = 0; child_row < parent->rowCount(); ++child_row) { - // This bounds check prevents a crash, and this was originally a safety check w/ showed if it failed, - // however with further testing w/ restoring default functionality, it would work yet still display, so was changed to a regular Success!. + // This bounds check prevents a crash. if (hotkey_index >= total_default_hotkeys) { QMessageBox::information(this, tr("Success!"), - tr("Citron's Default hotkey entries have been restored!")); + tr("Citron's Default hotkey entries have been restored!")); return; } diff --git a/src/citron/configuration/configure_hotkeys.h b/src/citron/configuration/configure_hotkeys.h index 20ea3b515..834b18c43 100644 --- a/src/citron/configuration/configure_hotkeys.h +++ b/src/citron/configuration/configure_hotkeys.h @@ -1,11 +1,7 @@ -// SPDX-FileCopyrightText: 2017 Citra Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - #include #include #include +#include "citron/hotkey_profile_manager.h" namespace Common { class ParamPackage; @@ -28,21 +24,31 @@ class ConfigureHotkeys : public QWidget { Q_OBJECT public: - explicit ConfigureHotkeys(Core::HID::HIDCore& hid_core_, QWidget* parent = nullptr); + explicit ConfigureHotkeys(HotkeyRegistry& registry, Core::HID::HIDCore& hid_core_, + QWidget* parent = nullptr); ~ConfigureHotkeys() override; - void ApplyConfiguration(HotkeyRegistry& registry); + void ApplyConfiguration(); /** - * Populates the hotkey list widget using data from the provided registry. + * Populates the hotkey list widget using data from the provided profiles. * Called every time the Configure dialog is opened. - * @param registry The HotkeyRegistry whose data is used to populate the list. + * @param profiles The UserHotkeyProfiles used to populate the list. */ - void Populate(const HotkeyRegistry& registry); + void Populate(); + +private slots: + void OnCreateProfile(); + void OnDeleteProfile(); + void OnRenameProfile(); + void OnImportProfile(); + void OnExportProfile(); + void OnProfileChanged(int index); private: void changeEvent(QEvent* event) override; void RetranslateUI(); + void UpdateProfileList(); void Configure(QModelIndex index); void ConfigureController(QModelIndex index); @@ -59,6 +65,8 @@ private: QString GetButtonCombinationName(Core::HID::NpadButton button, bool home, bool capture) const; std::unique_ptr ui; + Hotkey::ProfileManager profile_manager; + HotkeyRegistry& registry; QStandardItemModel* model; diff --git a/src/citron/configuration/configure_hotkeys.ui b/src/citron/configuration/configure_hotkeys.ui index a6902a5d8..c8945769f 100644 --- a/src/citron/configuration/configure_hotkeys.ui +++ b/src/citron/configuration/configure_hotkeys.ui @@ -18,16 +18,65 @@ - + - + - Double-click on a binding to change it. + Hotkey Profile: - + + + + 0 + 0 + + + + + + + + + + + + New + + + + + + + Delete + + + + + + + Rename + + + + + + + Import + + + + + + + Export + + + + + Qt::Horizontal @@ -48,6 +97,24 @@ + + + 0 + 0 + + + + + 139 + 0 + + + + + 139 + 16777215 + + Restore Defaults @@ -55,6 +122,13 @@ + + + + Double-click on a binding to change it. + + + diff --git a/src/citron/hotkey_profile_manager.cpp b/src/citron/hotkey_profile_manager.cpp new file mode 100644 index 000000000..ffd72d738 --- /dev/null +++ b/src/citron/hotkey_profile_manager.cpp @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "citron/hotkey_profile_manager.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" + +namespace Hotkey { + +ProfileManager::ProfileManager() { + Load(); +} + +ProfileManager::~ProfileManager() = default; + +static std::string GetSaveFilePath() { + const auto save_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir); // Saved in ConfigDir now + return Common::FS::PathToUTF8String(save_dir / "hotkey_profiles.json"); +} + +// JSON Serialization Helpers +static QJsonObject SerializeShortcut(const BackendShortcut& shortcut) { + QJsonObject obj; + obj[QStringLiteral("name")] = QString::fromStdString(shortcut.name); + obj[QStringLiteral("group")] = QString::fromStdString(shortcut.group); + obj[QStringLiteral("keyseq")] = QString::fromStdString(shortcut.shortcut.keyseq); + obj[QStringLiteral("controller_keyseq")] = + QString::fromStdString(shortcut.shortcut.controller_keyseq); + obj[QStringLiteral("context")] = shortcut.shortcut.context; + obj[QStringLiteral("repeat")] = shortcut.shortcut.repeat; + return obj; +} + +static BackendShortcut DeserializeShortcut(const QJsonObject& obj) { + BackendShortcut s; + s.name = obj[QStringLiteral("name")].toString().toStdString(); + s.group = obj[QStringLiteral("group")].toString().toStdString(); + s.shortcut.keyseq = obj[QStringLiteral("keyseq")].toString().toStdString(); + s.shortcut.controller_keyseq = + obj[QStringLiteral("controller_keyseq")].toString().toStdString(); + s.shortcut.context = obj[QStringLiteral("context")].toInt(); + s.shortcut.repeat = obj[QStringLiteral("repeat")].toBool(); + return s; +} + +void ProfileManager::Load() { + const auto path = GetSaveFilePath(); + QFile file(QString::fromStdString(path)); + if (!file.open(QIODevice::ReadOnly)) { + LOG_INFO(Config, "hotkey_profiles.json not found, creating new."); + return; + } + + const QByteArray data = file.readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(data); + const QJsonObject root = doc.object(); + + profiles.profiles.clear(); + + if (root.contains(QStringLiteral("current_profile"))) { + profiles.current_profile = root[QStringLiteral("current_profile")].toString().toStdString(); + } + + if (root.contains(QStringLiteral("profiles"))) { + const QJsonObject profiles_obj = root[QStringLiteral("profiles")].toObject(); + for (auto it = profiles_obj.begin(); it != profiles_obj.end(); ++it) { + const QString profile_name = it.key(); + const QJsonArray shortcuts_arr = it.value().toArray(); + std::vector shortcuts; + for (const auto& val : shortcuts_arr) { + shortcuts.push_back(DeserializeShortcut(val.toObject())); + } + profiles.profiles[profile_name.toStdString()] = shortcuts; + } + } + + // Ensure default profile exists + if (profiles.profiles.empty()) { + profiles.profiles["Default"] = {}; + } +} + +void ProfileManager::Save() { + const auto path = GetSaveFilePath(); + QFile file(QString::fromStdString(path)); + if (!file.open(QIODevice::WriteOnly)) { + LOG_ERROR(Config, "Failed to open hotkey_profiles.json for writing."); + return; + } + + QJsonObject root; + root[QStringLiteral("current_profile")] = QString::fromStdString(profiles.current_profile); + + QJsonObject profiles_obj; + for (const auto& [name, shortcuts] : profiles.profiles) { + QJsonArray shortcuts_arr; + for (const auto& s : shortcuts) { + shortcuts_arr.append(SerializeShortcut(s)); + } + profiles_obj[QString::fromStdString(name)] = shortcuts_arr; + } + root[QStringLiteral("profiles")] = profiles_obj; + + file.write(QJsonDocument(root).toJson()); +} + +bool ProfileManager::CreateProfile(const std::string& profile_name) { + if (profile_name.empty()) + return false; + + if (profiles.profiles.size() >= MAX_PROFILES) { + return false; + } + if (profiles.profiles.count(profile_name)) { + return false; // Already exists + } + + profiles.profiles[profile_name] = {}; // Create empty, populated later by UI + Save(); + return true; +} + +bool ProfileManager::DeleteProfile(const std::string& profile_name) { + if (profile_name == "Default") + return false; // Cannot delete default + + if (profiles.profiles.erase(profile_name)) { + if (profiles.current_profile == profile_name) { + profiles.current_profile = "Default"; + } + Save(); + return true; + } + return false; +} + +bool ProfileManager::RenameProfile(const std::string& old_name, const std::string& new_name) { + if (old_name == "Default") + return false; // Cannot rename default + if (new_name.empty()) + return false; + + if (!profiles.profiles.count(old_name)) + return false; + if (profiles.profiles.count(new_name)) + return false; + + auto node = profiles.profiles.extract(old_name); + node.key() = new_name; + profiles.profiles.insert(std::move(node)); + + if (profiles.current_profile == old_name) { + profiles.current_profile = new_name; + } + Save(); + return true; +} + +bool ProfileManager::SetCurrentProfile(const std::string& profile_name) { + if (!profiles.profiles.count(profile_name)) + return false; + + profiles.current_profile = profile_name; + Save(); + return true; +} + +void ProfileManager::SetProfileShortcuts(const std::string& profile_name, + const std::vector& shortcuts) { + if (profiles.profiles.count(profile_name)) { + profiles.profiles[profile_name] = shortcuts; + } +} + +bool ProfileManager::ExportProfile(const std::string& profile_name, const std::string& file_path) { + if (!profiles.profiles.count(profile_name)) + return false; + + QJsonObject root; + root[QStringLiteral("name")] = QString::fromStdString(profile_name); + + QJsonArray shortcuts_arr; + for (const auto& s : profiles.profiles.at(profile_name)) { + shortcuts_arr.append(SerializeShortcut(s)); + } + root[QStringLiteral("shortcuts")] = shortcuts_arr; + + QFile file(QString::fromStdString(file_path)); + if (!file.open(QIODevice::WriteOnly)) + return false; + + file.write(QJsonDocument(root).toJson()); + return true; +} + +bool ProfileManager::ImportProfile(const std::string& file_path) { + QFile file(QString::fromStdString(file_path)); + if (!file.open(QIODevice::ReadOnly)) + return false; + + const QByteArray data = file.readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(data); + const QJsonObject root = doc.object(); + + if (!root.contains(QStringLiteral("name")) || !root.contains(QStringLiteral("shortcuts"))) + return false; + + std::string profile_name = root[QStringLiteral("name")].toString().toStdString(); + + // Handle name collision + if (profiles.profiles.count(profile_name)) { + profile_name += " (Imported)"; + } + + if (profiles.profiles.size() >= MAX_PROFILES) + return false; + + std::vector shortcuts; + const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray(); + for (const auto& val : arr) { + shortcuts.push_back(DeserializeShortcut(val.toObject())); + } + + profiles.profiles[profile_name] = shortcuts; + Save(); + return true; +} + +} // namespace Hotkey diff --git a/src/citron/hotkey_profile_manager.h b/src/citron/hotkey_profile_manager.h new file mode 100644 index 000000000..631d159ae --- /dev/null +++ b/src/citron/hotkey_profile_manager.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include "common/uuid.h" + +namespace Hotkey { + +// A backend-only representation of a shortcut, free of any Qt types. +struct BackendContextualShortcut { + std::string keyseq; + std::string controller_keyseq; + int context; + bool repeat; +}; + +struct BackendShortcut { + std::string name; + std::string group; + BackendContextualShortcut shortcut; +}; + +// Contains all hotkey profile data for a single user +struct UserHotkeyProfiles { + std::map> profiles; + std::string current_profile = "Default"; +}; + +class ProfileManager { +public: + ProfileManager(); + ~ProfileManager(); + + // Profile Access + const UserHotkeyProfiles& GetProfiles() const { + return profiles; + } + + // Profile Management + bool CreateProfile(const std::string& profile_name); + bool DeleteProfile(const std::string& profile_name); + bool RenameProfile(const std::string& old_name, const std::string& new_name); + bool SetCurrentProfile(const std::string& profile_name); + void SetProfileShortcuts(const std::string& profile_name, + const std::vector& shortcuts); + + // Import/Export + bool ExportProfile(const std::string& profile_name, const std::string& file_path); + bool ImportProfile(const std::string& file_path); + + // IO + void Load(); + void Save(); + + // Constants + static constexpr size_t MAX_PROFILES = 5; + +private: + // Global profiles data + UserHotkeyProfiles profiles; +}; + +} // namespace Hotkey From 105aa1dcb489afdc95fd6c47613b0b83823e3158 Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 08:55:36 -0500 Subject: [PATCH 05/16] Fix Compiler Issues Multiplatform --- .../configure_per_game_cheats.cpp | 68 +++++++++++++++---- ...nly_application_control_data_interface.cpp | 27 ++++++++ 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/citron/configuration/configure_per_game_cheats.cpp b/src/citron/configuration/configure_per_game_cheats.cpp index 35310d094..47d6cb2a3 100644 --- a/src/citron/configuration/configure_per_game_cheats.cpp +++ b/src/citron/configuration/configure_per_game_cheats.cpp @@ -8,14 +8,21 @@ #include #include -#include +#include +#include #include +#include +#include +#include #include #include #include #include #include +#include +#include "citron/configuration/configure_per_game_cheats.h" +#include "common/fs/path_util.h" #include "common/hex_util.h" #include "common/settings.h" #include "common/string_util.h" @@ -28,7 +35,6 @@ #include "core/loader/loader.h" #include "core/memory/cheat_engine.h" #include "ui_configure_per_game_cheats.h" -#include "citron/configuration/configure_per_game_cheats.h" ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* parent) : QWidget(parent), ui{std::make_unique()}, system{system_} { @@ -46,7 +52,10 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p tree_view->setSortingEnabled(true); tree_view->setEditTriggers(QHeaderView::NoEditTriggers); tree_view->setUniformRowHeights(true); - tree_view->setContextMenuPolicy(Qt::NoContextMenu); + tree_view->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(tree_view, &QTreeView::customContextMenuRequested, this, + &ConfigurePerGameCheats::OnContextMenu); item_model->insertColumns(0, 1); item_model->setHeaderData(0, Qt::Horizontal, tr("Cheat Name")); @@ -64,9 +73,11 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p enable_all_button = new QPushButton(tr("Enable All")); disable_all_button = new QPushButton(tr("Disable All")); save_button = new QPushButton(tr("Save")); + refresh_button = new QPushButton(tr("Refresh")); button_layout->addWidget(enable_all_button); button_layout->addWidget(disable_all_button); + button_layout->addWidget(refresh_button); button_layout->addStretch(); button_layout->addWidget(save_button); @@ -83,8 +94,8 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p &ConfigurePerGameCheats::EnableAllCheats); connect(disable_all_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::DisableAllCheats); - connect(save_button, &QPushButton::clicked, this, - &ConfigurePerGameCheats::SaveCheatSettings); + connect(save_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::SaveCheatSettings); + connect(refresh_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::RefreshCheats); } ConfigurePerGameCheats::~ConfigurePerGameCheats() = default; @@ -194,7 +205,8 @@ void ConfigurePerGameCheats::LoadConfiguration() { FileSys::XCI xci(file, title_id, 0); if (xci.GetStatus() == Loader::ResultStatus::Success) { auto program_nca = xci.GetNCAByType(FileSys::NCAContentType::Program); - if (program_nca && program_nca->GetStatus() == Loader::ResultStatus::Success) { + if (program_nca && + program_nca->GetStatus() == Loader::ResultStatus::Success) { auto exefs = program_nca->GetExeFS(); if (exefs) { main_nso = exefs->GetFile("main"); @@ -241,7 +253,8 @@ void ConfigurePerGameCheats::LoadConfiguration() { if (load_dir) { auto patch_dirs = load_dir->GetSubdirectories(); for (const auto& subdir : patch_dirs) { - if (!subdir) continue; + if (!subdir) + continue; // Use case-insensitive directory search (same as FindSubdirectoryCaseless) FileSys::VirtualDir cheats_dir; @@ -288,8 +301,10 @@ void ConfigurePerGameCheats::LoadConfiguration() { try { // Pad to full 64 chars (32 bytes) with zeros // Keep the case as-is from the filename - auto full_build_id_hex = potential_build_id + std::string(48, '0'); - auto build_id_bytes = Common::HexStringToArray<0x20>(full_build_id_hex); + auto full_build_id_hex = + potential_build_id + std::string(48, '0'); + auto build_id_bytes = + Common::HexStringToArray<0x20>(full_build_id_hex); // Verify the result is not all zeros bool is_valid_result = false; @@ -311,7 +326,8 @@ void ConfigurePerGameCheats::LoadConfiguration() { } } } - if (has_build_id) break; + if (has_build_id) + break; } } } @@ -353,9 +369,9 @@ void ConfigurePerGameCheats::LoadConfiguration() { // Add cheats to tree view for (const auto& cheat : cheats) { // Extract cheat name from readable_name (null-terminated) - const std::string cheat_name_str(cheat.definition.readable_name.data(), - strnlen(cheat.definition.readable_name.data(), - cheat.definition.readable_name.size())); + const std::string cheat_name_str( + cheat.definition.readable_name.data(), + strnlen(cheat.definition.readable_name.data(), cheat.definition.readable_name.size())); // Skip empty cheat names or cheats with no opcodes if (cheat_name_str.empty() || cheat.definition.num_opcodes == 0) { @@ -369,7 +385,8 @@ void ConfigurePerGameCheats::LoadConfiguration() { cheat_item->setCheckable(true); // Check if cheat is disabled - const bool cheat_disabled = disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end(); + const bool cheat_disabled = + disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end(); cheat_item->setCheckState(cheat_disabled ? Qt::Unchecked : Qt::Checked); list_items.push_back(QList{cheat_item}); @@ -474,3 +491,26 @@ void ConfigurePerGameCheats::ReloadCheatEngine() const { const auto cheats = pm.CreateCheatList(current_build_id); cheat_engine->Reload(cheats); } + +void ConfigurePerGameCheats::OnContextMenu(const QPoint& pos) { + const auto index = tree_view->indexAt(pos); + if (!index.isValid()) { + return; + } + + QMenu context_menu; + + auto* open_folder_action = context_menu.addAction(tr("Open Cheats Folder")); + connect(open_folder_action, &QAction::triggered, this, [this] { + const auto cheats_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir) / + fmt::format("{:016X}", title_id) / "cheats"; + QDir().mkpath(QString::fromStdString(cheats_dir.string())); + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(cheats_dir.string()))); + }); + + context_menu.exec(tree_view->viewport()->mapToGlobal(pos)); +} + +void ConfigurePerGameCheats::RefreshCheats() { + LoadConfiguration(); +} diff --git a/src/core/hle/service/ns/read_only_application_control_data_interface.cpp b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp index d6a336bae..84af79133 100644 --- a/src/core/hle/service/ns/read_only_application_control_data_interface.cpp +++ b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp @@ -1,8 +1,29 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// Suppress warnings from stb headers - use compiler-specific pragmas +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4244) // conversion, possible loss of data +#pragma warning(disable : 4456) // declaration hides previous local declaration +#pragma warning(disable : 4457) // declaration hides function parameter +#pragma warning(disable : 4701) // potentially uninitialized local variable +#pragma warning(disable : 4703) // potentially uninitialized local pointer variable +#elif defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#pragma clang diagnostic ignored "-Wimplicit-int-conversion" +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#pragma clang diagnostic ignored "-Wfloat-conversion" +#pragma clang diagnostic ignored "-Wconversion" +#pragma clang diagnostic ignored "-Wsign-conversion" +#elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +#endif #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_STATIC @@ -14,7 +35,13 @@ #include #include +#ifdef _MSC_VER +#pragma warning(pop) +#elif defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) #pragma GCC diagnostic pop +#endif #include #include From d919f1da3b49bef5af4b91d982e0a53527e18ccd Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 09:38:36 -0500 Subject: [PATCH 06/16] MSVC Compiler Warnings --- src/citron/game_list.cpp | 7 +++---- src/citron/main.cpp | 8 ++++---- src/citron/util/controller_navigation.h | 2 ++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index cfaeeb1d0..9d06fbd79 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -1638,10 +1638,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri const QFileInfo file_info(qpath); QDesktopServices::openUrl(QUrl::fromLocalFile(file_info.absolutePath())); }); - connect(open_save_location, &QAction::triggered, - [this, program_id, game_name, copyWithProgress, path_str]() { - emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str); - }); + connect(open_save_location, &QAction::triggered, [this, program_id, path_str]() { + emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str); + }); connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() { const QString new_path = diff --git a/src/citron/main.cpp b/src/citron/main.cpp index b8d348c70..80ccfa406 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -4629,13 +4629,13 @@ void GMainWindow::LoadAmiibo(const QString& filename) { } void GMainWindow::OnOpenCitronFolder() { - QDesktopServices::openUrl(QUrl::fromLocalFile( - QString::fromStdString(Common::FS::GetCitronPath(Common::FS::CitronPath::CitronDir)))); + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString( + Common::FS::GetCitronPath(Common::FS::CitronPath::CitronDir).string()))); } void GMainWindow::OnOpenLogFolder() { - QDesktopServices::openUrl(QUrl::fromLocalFile( - QString::fromStdString(Common::FS::GetCitronPath(Common::FS::CitronPath::LogDir)))); + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString( + Common::FS::GetCitronPath(Common::FS::CitronPath::LogDir).string()))); } void GMainWindow::OnVerifyInstalledContents() { diff --git a/src/citron/util/controller_navigation.h b/src/citron/util/controller_navigation.h index 86e210368..f2f3da3dd 100644 --- a/src/citron/util/controller_navigation.h +++ b/src/citron/util/controller_navigation.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include From 280c75bef016a85ef2423af507807b00f780fdfe Mon Sep 17 00:00:00 2001 From: Collecting Date: Wed, 4 Feb 2026 15:57:50 +0100 Subject: [PATCH 07/16] Add missing voids Signed-off-by: Collecting --- src/citron/configuration/configure_per_game_cheats.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/citron/configuration/configure_per_game_cheats.h b/src/citron/configuration/configure_per_game_cheats.h index 1794b69c5..53cfe3de9 100644 --- a/src/citron/configuration/configure_per_game_cheats.h +++ b/src/citron/configuration/configure_per_game_cheats.h @@ -51,6 +51,8 @@ private: void SaveCheatSettings(); void SetAllCheats(bool enabled); void ReloadCheatEngine() const; + void OnContextMenu(const QPoint& pos); + void RefreshCheats(); std::unique_ptr ui; FileSys::VirtualFile file; @@ -61,6 +63,7 @@ private: QPushButton* enable_all_button; QPushButton* disable_all_button; QPushButton* save_button; + QPushButton* refresh_button; QVBoxLayout* layout; QTreeView* tree_view; From dc4153cb70a0d5993c96ed3f2a45e8a50eb529f2 Mon Sep 17 00:00:00 2001 From: Collecting Date: Wed, 4 Feb 2026 16:28:08 +0100 Subject: [PATCH 08/16] Remove Unused Variable MSVC likes to complain a lot but also my bad, forgot that I removed from all the other stuff. This should be last commit spam. Signed-off-by: Collecting --- src/citron/main.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 80ccfa406..195dface1 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6533,10 +6533,6 @@ void GMainWindow::RegisterAutoloaderContents() { continue; } - const auto it = disabled_addons.find(title_id_val); - const auto& disabled_for_game = - (it != disabled_addons.end()) ? it->second : std::vector{}; - const auto process_content_type = [&](const std::filesystem::path& content_path) { if (!Common::FS::IsDir(content_path)) return; From 4a731f208962d330f9c9429224aacb8d0c48ed90 Mon Sep 17 00:00:00 2001 From: Collecting Date: Wed, 4 Feb 2026 16:53:43 +0100 Subject: [PATCH 09/16] More Unused Variables needing to be removed Signed-off-by: Collecting --- src/citron/main.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 195dface1..cb635a497 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6512,7 +6512,6 @@ void GMainWindow::CheckForUpdatesAutomatically() { void GMainWindow::RegisterAutoloaderContents() { autoloader_provider->ClearAllEntries(); - const auto& disabled_addons = Settings::values.disabled_addons; const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir); const auto autoloader_root = sdmc_path / "autoloader"; @@ -6526,9 +6525,8 @@ void GMainWindow::RegisterAutoloaderContents() { if (!title_dir_entry.is_directory()) continue; - u64 title_id_val = 0; try { - title_id_val = std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); + std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); } catch (const std::invalid_argument&) { continue; } @@ -6542,7 +6540,7 @@ void GMainWindow::RegisterAutoloaderContents() { continue; const std::string mod_name = mod_dir_entry.path().filename().string(); - // Citron: We do NOT skip disabled content here. + // We do NOT skip disabled content here. // If we skip it here, it doesn't show up in the UI (Properties -> Add-ons), // making it impossible for the user to re-enable it. // The PatchManager (core/file_sys/patch_manager.cpp) handles the actual enforcement From 53fe0174b0eb07447765b05e766cc6f8e4f1d78e Mon Sep 17 00:00:00 2001 From: Collecting Date: Wed, 4 Feb 2026 17:33:12 +0100 Subject: [PATCH 10/16] No wonder why I use Linux and not Windows Windows is insanely brutal Signed-off-by: Collecting --- src/citron/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index cb635a497..d3f2d28ea 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6526,7 +6526,8 @@ void GMainWindow::RegisterAutoloaderContents() { continue; try { - std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); + [[maybe_unused]] auto val = + std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); } catch (const std::invalid_argument&) { continue; } From 23e7ea382ab9cca694d786a3d667284e937209fb Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 13:50:01 -0500 Subject: [PATCH 11/16] Fix Hotkey Saving Logic --- .../configuration/configure_hotkeys.cpp | 140 ++++++++++++------ 1 file changed, 93 insertions(+), 47 deletions(-) diff --git a/src/citron/configuration/configure_hotkeys.cpp b/src/citron/configuration/configure_hotkeys.cpp index 1a18a099f..0176f25ce 100644 --- a/src/citron/configuration/configure_hotkeys.cpp +++ b/src/citron/configuration/configure_hotkeys.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -137,17 +138,26 @@ void ConfigureHotkeys::Populate() { auto* action_item = new QStandardItem(QCoreApplication::translate( "Hotkeys", qPrintable(QString::fromStdString(action_name)))); action_item->setEditable(false); - action_item->setData(QString::fromStdString(action_name)); + action_item->setData(QString::fromStdString(action_name), Qt::UserRole); auto* keyseq_item = new QStandardItem(keyseq_str); - // Store raw keyseq string logic? - // The system likely expects QKeySequence string format. keyseq_item->setData(keyseq_str, Qt::UserRole); keyseq_item->setEditable(false); auto* controller_item = new QStandardItem(controller_keyseq_str); controller_item->setEditable(false); + // Store metadata (context and repeat) for saving later + int context = hotkey.context; + bool repeat = hotkey.repeat; + if (overrides.count({group_name, action_name})) { + const auto& overridden = overrides.at({group_name, action_name}); + context = overridden.shortcut.context; + repeat = overridden.shortcut.repeat; + } + action_item->setData(context, Qt::UserRole + 1); + action_item->setData(repeat, Qt::UserRole + 2); + parent_item->appendRow({action_item, keyseq_item, controller_item}); } @@ -467,60 +477,73 @@ std::pair ConfigureHotkeys::IsUsedControllerKey(const QString& ke } void ConfigureHotkeys::ApplyConfiguration() { - // 1. Update the runtime UISettings (Registry) - // We iterate the model and match against UISettings::values.shortcuts - const auto& children = model->invisibleRootItem(); - for (int group_row = 0; group_row < children->rowCount(); group_row++) { - const auto& group_item = children->child(group_row); + // 1. Update the runtime HotkeyRegistry and UISettings + const auto& root = model->invisibleRootItem(); + std::vector new_ui_shortcuts; + + for (int group_row = 0; group_row < root->rowCount(); group_row++) { + const auto* group_item = root->child(group_row); + const std::string group_name = group_item->data().toString().toStdString(); + for (int row = 0; row < group_item->rowCount(); row++) { - const auto& action_item = group_item->child(row, name_column); - const auto& keyseq_item = group_item->child(row, hotkey_column); - const auto& controller_item = group_item->child(row, controller_column); + const auto* action_item = group_item->child(row, name_column); + const auto* keyseq_item = group_item->child(row, hotkey_column); + const auto* controller_item = group_item->child(row, controller_column); - const std::string group_name = group_item->data().toString().toStdString(); - const std::string action_name = action_item->data().toString().toStdString(); + const std::string action_name = + action_item->data(Qt::UserRole).toString().toStdString(); + const QString keyseq_str = keyseq_item->text(); + const std::string controller_keyseq = controller_item->text().toStdString(); + const int context = action_item->data(Qt::UserRole + 1).toInt(); + const bool repeat = action_item->data(Qt::UserRole + 2).toBool(); - // Update UISettings (Runtime) - for (auto& s : UISettings::values.shortcuts) { - if (s.group == group_name && s.name == action_name) { - s.shortcut.keyseq = keyseq_item->text().toStdString(); - s.shortcut.controller_keyseq = controller_item->text().toStdString(); - } + // Update Registry + auto& hk = registry.hotkey_groups[group_name][action_name]; + hk.keyseq = QKeySequence::fromString(keyseq_str, QKeySequence::NativeText); + hk.controller_keyseq = controller_keyseq; + hk.context = static_cast(context); + hk.repeat = repeat; + + if (hk.shortcut) { + hk.shortcut->setKey(hk.keyseq); } + if (hk.controller_shortcut) { + hk.controller_shortcut->SetKey(hk.controller_keyseq); + } + + // Sync with UISettings::values.shortcuts (only if modified from default) + // Actually, registry.SaveHotkeys() handles the "is_modified" check, + // but we'll collect them here for completeness if needed or just call SaveHotkeys. } } + // This will correctly populate UISettings::values.shortcuts based on current registry state + registry.SaveHotkeys(); + // 2. Update the ProfileManager (Storage) const std::string current_profile_name = profile_manager.GetProfiles().current_profile; - // We need to modify the profile in the manager. GetProfiles() returns const ref. - // We need a method to UpdateProfile or we need to cast away const (bad) or rely on reference if - // GetProfiles wasn't const. The previous implementation of GetProfiles was const. - // ProfileManager needs a method `UpdateCurrentProfile(vector)`? - // Or we can just use the internal map if we were friends. - // Reconstructing BackendShortcuts from UI std::vector new_shortcuts; - for (int group_row = 0; group_row < children->rowCount(); group_row++) { - const auto& group_item = children->child(group_row); + + for (int group_row = 0; group_row < root->rowCount(); group_row++) { + const auto* group_item = root->child(group_row); + const std::string group_name = group_item->data().toString().toStdString(); + for (int row = 0; row < group_item->rowCount(); row++) { - const auto& action_item = group_item->child(row, name_column); - const auto& keyseq_item = group_item->child(row, hotkey_column); - const auto& controller_item = group_item->child(row, controller_column); + const auto* action_item = group_item->child(row, name_column); + const auto* keyseq_item = group_item->child(row, hotkey_column); + const auto* controller_item = group_item->child(row, controller_column); Hotkey::BackendShortcut s; - s.group = group_item->data().toString().toStdString(); - s.name = action_item->data().toString().toStdString(); - s.shortcut.keyseq = keyseq_item->text().toStdString(); + s.group = group_name; + s.name = action_item->data(Qt::UserRole).toString().toStdString(); + s.shortcut.keyseq = + QKeySequence::fromString(keyseq_item->text(), QKeySequence::NativeText) + .toString() + .toStdString(); s.shortcut.controller_keyseq = controller_item->text().toStdString(); - // Context/Repeat need to be preserved from UserRole data - // For now, let's grab from UISettings since we just updated it or match it. + s.shortcut.context = action_item->data(Qt::UserRole + 1).toInt(); + s.shortcut.repeat = action_item->data(Qt::UserRole + 2).toBool(); - for (const auto& original : UISettings::values.shortcuts) { - if (original.group == s.group && original.name == s.name) { - s.shortcut.context = original.shortcut.context; - s.shortcut.repeat = original.shortcut.repeat; - break; - } - } new_shortcuts.push_back(s); } } @@ -598,8 +621,19 @@ void ConfigureHotkeys::PopupContextMenu(const QPoint& menu_location) { } void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) { - const QString& default_key_sequence = - QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.controller_keyseq); + const auto* group_item = model->itemFromIndex(index.parent()); + const auto* action_item = group_item->child(index.row(), name_column); + const std::string group_name = group_item->data().toString().toStdString(); + const std::string action_name = action_item->data().toString().toStdString(); + + QString default_key_sequence; + for (const auto& def : UISettings::default_hotkeys) { + if (def.group == group_name && def.name == action_name) { + default_key_sequence = QString::fromStdString(def.shortcut.controller_keyseq); + break; + } + } + const auto [key_sequence_used, used_action] = IsUsedControllerKey(default_key_sequence); if (key_sequence_used && default_key_sequence != model->data(index).toString()) { @@ -612,9 +646,21 @@ void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) { } void ConfigureHotkeys::RestoreHotkey(QModelIndex index) { - const QKeySequence& default_key_sequence = QKeySequence::fromString( - QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.keyseq), - QKeySequence::NativeText); + const auto* group_item = model->itemFromIndex(index.parent()); + const auto* action_item = group_item->child(index.row(), name_column); + const std::string group_name = group_item->data().toString().toStdString(); + const std::string action_name = action_item->data().toString().toStdString(); + + QString default_key_str; + for (const auto& def : UISettings::default_hotkeys) { + if (def.group == group_name && def.name == action_name) { + default_key_str = QString::fromStdString(def.shortcut.keyseq); + break; + } + } + + const QKeySequence& default_key_sequence = + QKeySequence::fromString(default_key_str, QKeySequence::NativeText); const auto [key_sequence_used, used_action] = IsUsedKey(default_key_sequence); if (key_sequence_used && default_key_sequence != QKeySequence(model->data(index).toString())) { From be0e131113d5860349207ac7ec6a1b1bfc5408c5 Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 19:26:27 -0500 Subject: [PATCH 12/16] Fixup Saving/Profiling logic --- .../configuration/configure_hotkeys.cpp | 275 +++++++++++------- src/citron/configuration/configure_hotkeys.h | 5 +- src/citron/hotkey_profile_manager.cpp | 27 +- src/citron/hotkey_profile_manager.h | 5 + src/citron/hotkeys.cpp | 23 +- 5 files changed, 208 insertions(+), 127 deletions(-) diff --git a/src/citron/configuration/configure_hotkeys.cpp b/src/citron/configuration/configure_hotkeys.cpp index 0176f25ce..8f6a22d65 100644 --- a/src/citron/configuration/configure_hotkeys.cpp +++ b/src/citron/configuration/configure_hotkeys.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -93,14 +94,20 @@ ConfigureHotkeys::ConfigureHotkeys(HotkeyRegistry& registry_, Core::HID::HIDCore ConfigureHotkeys::~ConfigureHotkeys() = default; -void ConfigureHotkeys::Populate() { +void ConfigureHotkeys::Populate(const std::string& profile_name) { const auto& profiles = profile_manager.GetProfiles(); - const auto& current_profile_name = profiles.current_profile; + std::string target_profile = profile_name; + if (target_profile.empty()) { + target_profile = ui->combo_box_profile->currentText().toStdString(); + } + if (target_profile.empty()) { + target_profile = profiles.current_profile; + } // Use default if current profile missing (safety) std::vector current_shortcuts; - if (profiles.profiles.count(current_profile_name)) { - current_shortcuts = profiles.profiles.at(current_profile_name); + if (profiles.profiles.count(target_profile)) { + current_shortcuts = profiles.profiles.at(target_profile); } else if (profiles.profiles.count("Default")) { current_shortcuts = profiles.profiles.at("Default"); } @@ -119,18 +126,19 @@ void ConfigureHotkeys::Populate() { auto* parent_item = new QStandardItem( QCoreApplication::translate("Hotkeys", qPrintable(QString::fromStdString(group_name)))); parent_item->setEditable(false); - parent_item->setData(QString::fromStdString(group_name)); + parent_item->setData(QString::fromStdString(group_name), Qt::UserRole); model->appendRow(parent_item); for (const auto& [action_name, hotkey] : group_map) { // Determine values (Registry Default vs Profile Override) QString keyseq_str = hotkey.keyseq.toString(QKeySequence::NativeText); + QString portable_keyseq = hotkey.keyseq.toString(QKeySequence::PortableText); QString controller_keyseq_str = QString::fromStdString(hotkey.controller_keyseq); if (overrides.count({group_name, action_name})) { const auto& overridden = overrides.at({group_name, action_name}); - keyseq_str = QKeySequence(QString::fromStdString(overridden.shortcut.keyseq)) - .toString(QKeySequence::NativeText); + portable_keyseq = QString::fromStdString(overridden.shortcut.keyseq); + keyseq_str = QKeySequence(portable_keyseq).toString(QKeySequence::NativeText); controller_keyseq_str = QString::fromStdString(overridden.shortcut.controller_keyseq); } @@ -141,7 +149,7 @@ void ConfigureHotkeys::Populate() { action_item->setData(QString::fromStdString(action_name), Qt::UserRole); auto* keyseq_item = new QStandardItem(keyseq_str); - keyseq_item->setData(keyseq_str, Qt::UserRole); + keyseq_item->setData(portable_keyseq, Qt::UserRole); keyseq_item->setEditable(false); auto* controller_item = new QStandardItem(controller_keyseq_str); @@ -238,32 +246,69 @@ void ConfigureHotkeys::OnRenameProfile() { void ConfigureHotkeys::OnImportProfile() { QString fileName = QFileDialog::getOpenFileName(this, tr("Import Profile"), QString(), tr("JSON Files (*.json)")); - if (!fileName.isEmpty()) { - if (profile_manager.ImportProfile(fileName.toStdString())) { - UpdateProfileList(); - } else { - QMessageBox::warning(this, tr("Error"), tr("Failed to import profile.")); - } + if (fileName.isEmpty()) + return; + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error"), tr("Failed to open file for reading.")); + return; } + + const QByteArray jsonData = file.readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(jsonData); + if (!doc.isObject()) { + QMessageBox::warning(this, tr("Error"), tr("Invalid profile format.")); + return; + } + + const QJsonObject root = doc.object(); + if (!root.contains(QStringLiteral("shortcuts"))) { + QMessageBox::warning(this, tr("Error"), tr("Invalid profile file (missing shortcuts).")); + return; + } + + std::vector shortcuts; + const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray(); + for (const auto& val : arr) { + shortcuts.push_back(Hotkey::ProfileManager::DeserializeShortcut(val.toObject())); + } + + ApplyShortcutsToModel(shortcuts); } void ConfigureHotkeys::OnExportProfile() { QString current = ui->combo_box_profile->currentText(); QString fileName = QFileDialog::getSaveFileName( this, tr("Export Profile"), current + QStringLiteral(".json"), tr("JSON Files (*.json)")); - if (!fileName.isEmpty()) { - if (!profile_manager.ExportProfile(current.toStdString(), fileName.toStdString())) { - QMessageBox::warning(this, tr("Error"), tr("Failed to export profile.")); - } + if (fileName.isEmpty()) + return; + + const std::vector shortcuts = GatherShortcutsFromUI(); + + QJsonObject root_obj; + root_obj[QStringLiteral("name")] = current; + QJsonArray shortcuts_arr; + for (const auto& s : shortcuts) { + shortcuts_arr.append(Hotkey::ProfileManager::SerializeShortcut(s)); } + root_obj[QStringLiteral("shortcuts")] = shortcuts_arr; + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + QMessageBox::warning(this, tr("Error"), tr("Failed to open file for writing.")); + return; + } + + file.write(QJsonDocument(root_obj).toJson()); } void ConfigureHotkeys::OnProfileChanged(int index) { if (index == -1) return; const std::string name = ui->combo_box_profile->currentText().toStdString(); - profile_manager.SetCurrentProfile(name); - Populate(); + // Decoupled from permanent SetCurrentProfile to ensure "stagnant" behavior. + Populate(name); } void ConfigureHotkeys::changeEvent(QEvent* event) { @@ -322,6 +367,7 @@ void ConfigureHotkeys::Configure(QModelIndex index) { tr("The entered key sequence is already assigned to: %1").arg(used_action)); } else { model->setData(index, key_sequence.toString(QKeySequence::NativeText)); + model->setData(index, key_sequence.toString(QKeySequence::PortableText), Qt::UserRole); } } void ConfigureHotkeys::ConfigureController(QModelIndex index) { @@ -477,104 +523,60 @@ std::pair ConfigureHotkeys::IsUsedControllerKey(const QString& ke } void ConfigureHotkeys::ApplyConfiguration() { - // 1. Update the runtime HotkeyRegistry and UISettings - const auto& root = model->invisibleRootItem(); - std::vector new_ui_shortcuts; + // 1. Sync the current profile selection permanently + const std::string current_profile_name = ui->combo_box_profile->currentText().toStdString(); + profile_manager.SetCurrentProfile(current_profile_name); - for (int group_row = 0; group_row < root->rowCount(); group_row++) { - const auto* group_item = root->child(group_row); - const std::string group_name = group_item->data().toString().toStdString(); + // 2. Update the runtime HotkeyRegistry and UISettings + const auto shortcuts = GatherShortcutsFromUI(); - for (int row = 0; row < group_item->rowCount(); row++) { - const auto* action_item = group_item->child(row, name_column); - const auto* keyseq_item = group_item->child(row, hotkey_column); - const auto* controller_item = group_item->child(row, controller_column); + for (const auto& s : shortcuts) { + // Update Registry + auto& hk = registry.hotkey_groups[s.group][s.name]; + hk.keyseq = QKeySequence::fromString(QString::fromStdString(s.shortcut.keyseq)); + hk.controller_keyseq = s.shortcut.controller_keyseq; + hk.context = static_cast(s.shortcut.context); + hk.repeat = s.shortcut.repeat; - const std::string action_name = - action_item->data(Qt::UserRole).toString().toStdString(); - const QString keyseq_str = keyseq_item->text(); - const std::string controller_keyseq = controller_item->text().toStdString(); - const int context = action_item->data(Qt::UserRole + 1).toInt(); - const bool repeat = action_item->data(Qt::UserRole + 2).toBool(); - - // Update Registry - auto& hk = registry.hotkey_groups[group_name][action_name]; - hk.keyseq = QKeySequence::fromString(keyseq_str, QKeySequence::NativeText); - hk.controller_keyseq = controller_keyseq; - hk.context = static_cast(context); - hk.repeat = repeat; - - if (hk.shortcut) { - hk.shortcut->setKey(hk.keyseq); - } - if (hk.controller_shortcut) { - hk.controller_shortcut->SetKey(hk.controller_keyseq); - } - - // Sync with UISettings::values.shortcuts (only if modified from default) - // Actually, registry.SaveHotkeys() handles the "is_modified" check, - // but we'll collect them here for completeness if needed or just call SaveHotkeys. + if (hk.shortcut) { + hk.shortcut->setKey(hk.keyseq); + } + if (hk.controller_shortcut) { + hk.controller_shortcut->SetKey(hk.controller_keyseq); } } // This will correctly populate UISettings::values.shortcuts based on current registry state registry.SaveHotkeys(); - // 2. Update the ProfileManager (Storage) - const std::string current_profile_name = profile_manager.GetProfiles().current_profile; - std::vector new_shortcuts; - - for (int group_row = 0; group_row < root->rowCount(); group_row++) { - const auto* group_item = root->child(group_row); - const std::string group_name = group_item->data().toString().toStdString(); - - for (int row = 0; row < group_item->rowCount(); row++) { - const auto* action_item = group_item->child(row, name_column); - const auto* keyseq_item = group_item->child(row, hotkey_column); - const auto* controller_item = group_item->child(row, controller_column); - - Hotkey::BackendShortcut s; - s.group = group_name; - s.name = action_item->data(Qt::UserRole).toString().toStdString(); - s.shortcut.keyseq = - QKeySequence::fromString(keyseq_item->text(), QKeySequence::NativeText) - .toString() - .toStdString(); - s.shortcut.controller_keyseq = controller_item->text().toStdString(); - s.shortcut.context = action_item->data(Qt::UserRole + 1).toInt(); - s.shortcut.repeat = action_item->data(Qt::UserRole + 2).toBool(); - - new_shortcuts.push_back(s); - } - } - - profile_manager.SetProfileShortcuts(current_profile_name, new_shortcuts); + // 3. Update the ProfileManager (Storage) + profile_manager.SetProfileShortcuts(current_profile_name, shortcuts); profile_manager.Save(); } void ConfigureHotkeys::RestoreDefaults() { - size_t hotkey_index = 0; - const size_t total_default_hotkeys = UISettings::default_hotkeys.size(); - for (int group_row = 0; group_row < model->rowCount(); ++group_row) { QStandardItem* parent = model->item(group_row, 0); + const std::string group_name = parent->data(Qt::UserRole).toString().toStdString(); for (int child_row = 0; child_row < parent->rowCount(); ++child_row) { - // This bounds check prevents a crash. - if (hotkey_index >= total_default_hotkeys) { - QMessageBox::information(this, tr("Success!"), - tr("Citron's Default hotkey entries have been restored!")); - return; + QStandardItem* action_item = parent->child(child_row, name_column); + const std::string action_name = + action_item->data(Qt::UserRole).toString().toStdString(); + + // Find default + for (const auto& def : UISettings::default_hotkeys) { + if (def.group == group_name && def.name == action_name) { + QStandardItem* hotkey_item = parent->child(child_row, hotkey_column); + hotkey_item->setText( + QKeySequence::fromString(QString::fromStdString(def.shortcut.keyseq)) + .toString(QKeySequence::NativeText)); + hotkey_item->setData(QString::fromStdString(def.shortcut.keyseq), Qt::UserRole); + parent->child(child_row, controller_column) + ->setText(QString::fromStdString(def.shortcut.controller_keyseq)); + break; + } } - - const auto& default_shortcut = UISettings::default_hotkeys[hotkey_index].shortcut; - - parent->child(child_row, hotkey_column) - ->setText(QString::fromStdString(default_shortcut.keyseq)); - parent->child(child_row, controller_column) - ->setText(QString::fromStdString(default_shortcut.controller_keyseq)); - - hotkey_index++; } } @@ -586,7 +588,9 @@ void ConfigureHotkeys::ClearAll() { const QStandardItem* parent = model->item(r, 0); for (int r2 = 0; r2 < parent->rowCount(); ++r2) { - model->item(r, 0)->child(r2, hotkey_column)->setText(QString{}); + QStandardItem* hotkey_item = model->item(r, 0)->child(r2, hotkey_column); + hotkey_item->setText(QString{}); + hotkey_item->setData(QString{}, Qt::UserRole); model->item(r, 0)->child(r2, controller_column)->setText(QString{}); } } @@ -623,8 +627,8 @@ void ConfigureHotkeys::PopupContextMenu(const QPoint& menu_location) { void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) { const auto* group_item = model->itemFromIndex(index.parent()); const auto* action_item = group_item->child(index.row(), name_column); - const std::string group_name = group_item->data().toString().toStdString(); - const std::string action_name = action_item->data().toString().toStdString(); + const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString(); + const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString(); QString default_key_sequence; for (const auto& def : UISettings::default_hotkeys) { @@ -648,8 +652,8 @@ void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) { void ConfigureHotkeys::RestoreHotkey(QModelIndex index) { const auto* group_item = model->itemFromIndex(index.parent()); const auto* action_item = group_item->child(index.row(), name_column); - const std::string group_name = group_item->data().toString().toStdString(); - const std::string action_name = action_item->data().toString().toStdString(); + const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString(); + const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString(); QString default_key_str; for (const auto& def : UISettings::default_hotkeys) { @@ -667,7 +671,64 @@ void ConfigureHotkeys::RestoreHotkey(QModelIndex index) { QMessageBox::warning( this, tr("Conflicting Key Sequence"), tr("The default key sequence is already assigned to: %1").arg(used_action)); - } else { - model->setData(index, default_key_sequence.toString(QKeySequence::NativeText)); + } +} + +std::vector ConfigureHotkeys::GatherShortcutsFromUI() const { + std::vector shortcuts; + const auto& root = model->invisibleRootItem(); + for (int group_row = 0; group_row < root->rowCount(); group_row++) { + const auto* group_item = root->child(group_row); + const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString(); + for (int row = 0; row < group_item->rowCount(); row++) { + const auto* action_item = group_item->child(row, name_column); + const auto* keyseq_item = group_item->child(row, hotkey_column); + const auto* controller_item = group_item->child(row, controller_column); + + Hotkey::BackendShortcut s; + s.group = group_name; + s.name = action_item->data(Qt::UserRole).toString().toStdString(); + s.shortcut.keyseq = keyseq_item->data(Qt::UserRole).toString().toStdString(); + s.shortcut.controller_keyseq = controller_item->text().toStdString(); + s.shortcut.context = action_item->data(Qt::UserRole + 1).toInt(); + s.shortcut.repeat = action_item->data(Qt::UserRole + 2).toBool(); + shortcuts.push_back(s); + } + } + return shortcuts; +} + +void ConfigureHotkeys::ApplyShortcutsToModel( + const std::vector& shortcuts) { + // Map for faster lookup + std::map, Hotkey::BackendShortcut> shortcut_map; + for (const auto& s : shortcuts) { + shortcut_map[{s.group, s.name}] = s; + } + + const auto& root = model->invisibleRootItem(); + for (int group_row = 0; group_row < root->rowCount(); group_row++) { + auto* group_item = root->child(group_row); + const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString(); + for (int row = 0; row < group_item->rowCount(); row++) { + auto* action_item = group_item->child(row, name_column); + const std::string action_name = + action_item->data(Qt::UserRole).toString().toStdString(); + + if (shortcut_map.count({group_name, action_name})) { + const auto& s = shortcut_map.at({group_name, action_name}); + QStandardItem* hotkey_item = group_item->child(row, hotkey_column); + hotkey_item->setText(QKeySequence(QString::fromStdString(s.shortcut.keyseq)) + .toString(QKeySequence::NativeText)); + hotkey_item->setData(QString::fromStdString(s.shortcut.keyseq), Qt::UserRole); + + model->setData(model->index(row, controller_column, group_item->index()), + QString::fromStdString(s.shortcut.controller_keyseq)); + model->setData(model->index(row, name_column, group_item->index()), + s.shortcut.context, Qt::UserRole + 1); + model->setData(model->index(row, name_column, group_item->index()), + s.shortcut.repeat, Qt::UserRole + 2); + } + } } } diff --git a/src/citron/configuration/configure_hotkeys.h b/src/citron/configuration/configure_hotkeys.h index 834b18c43..44393c42d 100644 --- a/src/citron/configuration/configure_hotkeys.h +++ b/src/citron/configuration/configure_hotkeys.h @@ -35,7 +35,7 @@ public: * Called every time the Configure dialog is opened. * @param profiles The UserHotkeyProfiles used to populate the list. */ - void Populate(); + void Populate(const std::string& profile_name = ""); private slots: void OnCreateProfile(); @@ -61,6 +61,9 @@ private: void RestoreControllerHotkey(QModelIndex index); void RestoreHotkey(QModelIndex index); + std::vector GatherShortcutsFromUI() const; + void ApplyShortcutsToModel(const std::vector& shortcuts); + void SetPollingResult(bool cancel); QString GetButtonCombinationName(Core::HID::NpadButton button, bool home, bool capture) const; diff --git a/src/citron/hotkey_profile_manager.cpp b/src/citron/hotkey_profile_manager.cpp index ffd72d738..bc6e91d5b 100644 --- a/src/citron/hotkey_profile_manager.cpp +++ b/src/citron/hotkey_profile_manager.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include #include #include @@ -29,7 +31,7 @@ static std::string GetSaveFilePath() { } // JSON Serialization Helpers -static QJsonObject SerializeShortcut(const BackendShortcut& shortcut) { +QJsonObject ProfileManager::SerializeShortcut(const BackendShortcut& shortcut) { QJsonObject obj; obj[QStringLiteral("name")] = QString::fromStdString(shortcut.name); obj[QStringLiteral("group")] = QString::fromStdString(shortcut.group); @@ -41,7 +43,7 @@ static QJsonObject SerializeShortcut(const BackendShortcut& shortcut) { return obj; } -static BackendShortcut DeserializeShortcut(const QJsonObject& obj) { +BackendShortcut ProfileManager::DeserializeShortcut(const QJsonObject& obj) { BackendShortcut s; s.name = obj[QStringLiteral("name")].toString().toStdString(); s.group = obj[QStringLiteral("group")].toString().toStdString(); @@ -204,26 +206,35 @@ bool ProfileManager::ExportProfile(const std::string& profile_name, const std::s } bool ProfileManager::ImportProfile(const std::string& file_path) { + return !ImportProfileAndGetFinalName(file_path).empty(); +} + +std::string ProfileManager::ImportProfileAndGetFinalName(const std::string& file_path) { QFile file(QString::fromStdString(file_path)); if (!file.open(QIODevice::ReadOnly)) - return false; + return {}; const QByteArray data = file.readAll(); const QJsonDocument doc = QJsonDocument::fromJson(data); const QJsonObject root = doc.object(); if (!root.contains(QStringLiteral("name")) || !root.contains(QStringLiteral("shortcuts"))) - return false; + return {}; std::string profile_name = root[QStringLiteral("name")].toString().toStdString(); + if (profile_name.empty()) { + profile_name = "Imported Profile"; + } // Handle name collision - if (profiles.profiles.count(profile_name)) { - profile_name += " (Imported)"; + std::string base_name = profile_name; + int suffix = 1; + while (profiles.profiles.count(profile_name)) { + profile_name = base_name + " (" + std::to_string(suffix++) + ")"; } if (profiles.profiles.size() >= MAX_PROFILES) - return false; + return {}; std::vector shortcuts; const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray(); @@ -233,7 +244,7 @@ bool ProfileManager::ImportProfile(const std::string& file_path) { profiles.profiles[profile_name] = shortcuts; Save(); - return true; + return profile_name; } } // namespace Hotkey diff --git a/src/citron/hotkey_profile_manager.h b/src/citron/hotkey_profile_manager.h index 631d159ae..19c1d2c89 100644 --- a/src/citron/hotkey_profile_manager.h +++ b/src/citron/hotkey_profile_manager.h @@ -53,6 +53,11 @@ public: // Import/Export bool ExportProfile(const std::string& profile_name, const std::string& file_path); bool ImportProfile(const std::string& file_path); + std::string ImportProfileAndGetFinalName(const std::string& file_path); + + // JSON Serialization Helpers + static QJsonObject SerializeShortcut(const BackendShortcut& shortcut); + static BackendShortcut DeserializeShortcut(const QJsonObject& obj); // IO void Load(); diff --git a/src/citron/hotkeys.cpp b/src/citron/hotkeys.cpp index b3a19014c..04d7bd462 100644 --- a/src/citron/hotkeys.cpp +++ b/src/citron/hotkeys.cpp @@ -7,9 +7,10 @@ #include #include -#include "hid_core/frontend/emulated_controller.h" #include "citron/hotkeys.h" #include "citron/uisettings.h" +#include "hid_core/frontend/emulated_controller.h" + HotkeyRegistry::HotkeyRegistry() = default; HotkeyRegistry::~HotkeyRegistry() = default; @@ -46,10 +47,10 @@ void HotkeyRegistry::SaveHotkeys() { if (is_modified) { UISettings::values.shortcuts.push_back( {action_name, group_name, - UISettings::ContextualShortcut( - {current_hotkey.keyseq.toString().toStdString(), - current_hotkey.controller_keyseq, current_hotkey.context, - current_hotkey.repeat})}); + UISettings::ContextualShortcut({current_hotkey.keyseq.toString().toStdString(), + current_hotkey.controller_keyseq, + current_hotkey.context, + current_hotkey.repeat})}); } } } @@ -70,8 +71,7 @@ void HotkeyRegistry::LoadHotkeys() { for (const auto& shortcut : UISettings::values.shortcuts) { Hotkey& hk = hotkey_groups[shortcut.group][shortcut.name]; if (!shortcut.shortcut.keyseq.empty()) { - hk.keyseq = QKeySequence::fromString(QString::fromStdString(shortcut.shortcut.keyseq), - QKeySequence::NativeText); + hk.keyseq = QKeySequence::fromString(QString::fromStdString(shortcut.shortcut.keyseq)); } else { // This is the fix: explicitly clear the key sequence if it was saved as empty. hk.keyseq = QKeySequence(); @@ -101,9 +101,9 @@ QShortcut* HotkeyRegistry::GetHotkey(const std::string& group, const std::string return hk.shortcut; } -ControllerShortcut* HotkeyRegistry::GetControllerHotkey(const std::string& group, - const std::string& action, - Core::HID::EmulatedController* controller) const { +ControllerShortcut* HotkeyRegistry::GetControllerHotkey( + const std::string& group, const std::string& action, + Core::HID::EmulatedController* controller) const { Hotkey& hk = hotkey_groups[group][action]; if (!hk.controller_shortcut) { @@ -114,7 +114,8 @@ ControllerShortcut* HotkeyRegistry::GetControllerHotkey(const std::string& group return hk.controller_shortcut; } -QKeySequence HotkeyRegistry::GetKeySequence(const std::string& group, const std::string& action) const { +QKeySequence HotkeyRegistry::GetKeySequence(const std::string& group, + const std::string& action) const { return hotkey_groups[group][action].keyseq; } From 1aa1ec9ee9b2c0d96a9f8ca1c42c69b3dec27f4a Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 19:54:52 -0500 Subject: [PATCH 13/16] Fix Updater Logic & Remove version.txt requirement w/ refined SCM --- src/citron/updater/updater_service.cpp | 329 ++++++++++++++++--------- src/citron/updater/updater_service.h | 40 ++- 2 files changed, 241 insertions(+), 128 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index f43902ad9..1b737cb2a 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -1,30 +1,30 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "citron/updater/updater_service.h" #include "citron/uisettings.h" -#include "common/logging/log.h" +#include "citron/updater/updater_service.h" #include "common/fs/path_util.h" +#include "common/logging/log.h" #include "common/scm_rev.h" #include -#include -#include -#include -#include -#include +#include +#include #include #include #include -#include +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include #include +#include +#include +#include +#include #ifdef CITRON_ENABLE_LIBARCHIVE #include @@ -35,17 +35,33 @@ #include #ifdef _WIN32 -#include #include +#include + #endif namespace Updater { -const std::string STABLE_UPDATE_URL = "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases"; -const std::string NIGHTLY_UPDATE_URL = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; +const std::string STABLE_UPDATE_URL = + "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases"; +const std::string NIGHTLY_UPDATE_URL = + "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; std::string ExtractCommitHash(const std::string& version_string) { - std::regex re("\\b([0-9a-fA-F]{7,40})\\b"); + // Hashes in git describe often start with 'g'. + // We match 7-40 hex characters, optionally preceded by 'g'. + std::regex re("(?:\\b|[gG])([0-9a-fA-F]{7,40})\\b"); + std::smatch match; + if (std::regex_search(version_string, match, re) && match.size() > 1) { + return match[1].str(); + } + return ""; +} + +std::string ExtractVersionTag(const std::string& version_string) { + // Matches tag parts like v1.2.3 or 2026.02.1 at the start of the string. + // We stop at the first hyphen to avoid the -count-gHASH part. + std::regex re("^v?([0-9.]+)"); std::smatch match; if (std::regex_search(version_string, match, re) && match.size() > 1) { return match[1].str(); @@ -65,7 +81,6 @@ QByteArray GetFileChecksum(const std::filesystem::path& file_path) { return QByteArray(); } - UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { network_manager = std::make_unique(this); InitializeSSL(); @@ -91,22 +106,27 @@ void UpdaterService::InitializeSSL() { // Check if SSL is supported if (!QSslSocket::supportsSsl()) { LOG_WARNING(Frontend, "SSL support not available"); - LOG_WARNING(Frontend, "Build-time SSL version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString()); - LOG_WARNING(Frontend, "Runtime SSL version: {}", QSslSocket::sslLibraryVersionString().toStdString()); + LOG_WARNING(Frontend, "Build-time SSL version: {}", + QSslSocket::sslLibraryBuildVersionString().toStdString()); + LOG_WARNING(Frontend, "Runtime SSL version: {}", + QSslSocket::sslLibraryVersionString().toStdString()); #ifdef _WIN32 // Try to provide helpful information about missing DLLs - std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + std::filesystem::path app_dir = + std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); std::filesystem::path crypto_dll = app_dir / "libcrypto-3-x64.dll"; std::filesystem::path ssl_dll = app_dir / "libssl-3-x64.dll"; - LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}", std::filesystem::exists(crypto_dll)); + LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}", + std::filesystem::exists(crypto_dll)); LOG_WARNING(Frontend, "libssl-3-x64.dll exists: {}", std::filesystem::exists(ssl_dll)); #endif return; } - LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); + LOG_INFO(Frontend, "SSL library version: {}", + QSslSocket::sslLibraryVersionString().toStdString()); QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); auto certs = QSslConfiguration::systemCaCertificates(); @@ -126,22 +146,27 @@ void UpdaterService::CheckForUpdates() { return; } QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString(); - std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; + QString channel = + settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString(); + std::string update_url = + (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString()); LOG_INFO(Frontend, "Checking for updates from: {}", update_url); QUrl url{QString::fromStdString(update_url)}; QNetworkRequest request{url}; request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); request.setRawHeader("Accept", QByteArrayLiteral("application/json")); - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); connect(current_reply, &QNetworkReply::finished, this, [this, channel]() { - if (!current_reply) return; + if (!current_reply) + return; if (current_reply->error() == QNetworkReply::NoError) { ParseUpdateResponse(current_reply->readAll(), channel); } else { - emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString())); + emit UpdateError( + QStringLiteral("Update check failed: %1").arg(current_reply->errorString())); } current_reply->deleteLater(); current_reply = nullptr; @@ -149,7 +174,8 @@ void UpdaterService::CheckForUpdates() { } void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { - if (!QSslSocket::supportsSsl()) return; + if (!QSslSocket::supportsSsl()) + return; QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfig.setProtocol(QSsl::SecureProtocols); @@ -173,7 +199,8 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { #ifdef _WIN32 if (!CreateBackup()) { - emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup")); + emit UpdateCompleted(UpdateResult::PermissionError, + QStringLiteral("Failed to create backup")); update_in_progress.store(false); return; } @@ -181,15 +208,18 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { QUrl url(QString::fromStdString(download_url)); QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); - connect(current_reply, &QNetworkReply::downloadProgress, this, &UpdaterService::OnDownloadProgress); + connect(current_reply, &QNetworkReply::downloadProgress, this, + &UpdaterService::OnDownloadProgress); connect(current_reply, &QNetworkReply::finished, this, &UpdaterService::OnDownloadFinished); connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); } void UpdaterService::CancelUpdate() { - if (!update_in_progress.load()) return; + if (!update_in_progress.load()) + return; cancel_requested.store(true); if (current_reply) { current_reply->abort(); @@ -201,21 +231,12 @@ void UpdaterService::CancelUpdate() { std::string UpdaterService::GetCurrentVersion() const { QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); + QString channel = + settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); - // If the user's setting is Nightly, we must ignore version.txt and only use the commit hash. - if (channel == QStringLiteral("Nightly")) { - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - std::string hash = ExtractCommitHash(build_version); - if (!hash.empty()) { - return hash; - } - } - return ""; // Fallback if no hash is found - } + const std::string build_version = Common::g_build_version; - // Otherwise (channel is Stable), we prioritize version.txt. + // First priority: version.txt (only relevant for Stable installations) std::filesystem::path search_path; #ifdef __linux__ const char* appimage_path_env = qgetenv("APPIMAGE").constData(); @@ -235,22 +256,37 @@ std::string UpdaterService::GetCurrentVersion() const { std::string version_from_file; std::getline(file, version_from_file); if (!version_from_file.empty()) { - return version_from_file; + // Trim trailing metadata/whitespace from version.txt (e.g. "1.0.0 (Release)") + return version_from_file.substr(0, version_from_file.find_first_of(" \t\r\n")); } } } - // Fallback for Stable channel: If version.txt is missing, use the commit hash. - // This allows a nightly build to correctly check for a stable update. - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - std::string hash = ExtractCommitHash(build_version); - if (!hash.empty()) { - return hash; + // If the user's setting is Nightly, we prioritize the commit hash. + if (channel == QStringLiteral("Nightly")) { + if (!build_version.empty()) { + std::string hash = ExtractCommitHash(build_version); + if (!hash.empty()) { + return hash; + } + } + } else { + // Otherwise (channel is Stable), we try to extract the tag from build_version if + // version.txt is missing. This happens when a Nightly user checks for Stable updates. + std::string tag = ExtractVersionTag(build_version); + if (!tag.empty()) { + return tag; } } - return ""; + // Common fallback: try to extract a hash if we haven't found a tag yet, + // otherwise just return the full build version. + std::string hash_fallback = ExtractCommitHash(build_version); + if (!hash_fallback.empty()) { + return hash_fallback; + } + + return build_version; } bool UpdaterService::IsUpdateInProgress() const { @@ -270,14 +306,17 @@ void UpdaterService::OnDownloadFinished() { QByteArray downloaded_data = current_reply->readAll(); QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); + QString channel = + settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); #if defined(_WIN32) - QString filename = QStringLiteral("citron_update_%1.zip").arg(QString::fromStdString(current_update_info.version)); + QString filename = QStringLiteral("citron_update_%1.zip") + .arg(QString::fromStdString(current_update_info.version)); std::filesystem::path download_path = temp_download_path / filename.toStdString(); QFile file(QString::fromStdString(download_path.string())); if (!file.open(QIODevice::WriteOnly)) { - emit UpdateCompleted(UpdateResult::Failed, QStringLiteral("Failed to save downloaded file")); + emit UpdateCompleted(UpdateResult::Failed, + QStringLiteral("Failed to save downloaded file")); update_in_progress.store(false); return; } @@ -293,7 +332,8 @@ void UpdaterService::OnDownloadFinished() { emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive...")); std::filesystem::path extract_path = temp_download_path / "extracted"; if (!ExtractArchive(download_path, extract_path)) { - emit UpdateCompleted(UpdateResult::ExtractionError, QStringLiteral("Failed to extract update archive")); + emit UpdateCompleted(UpdateResult::ExtractionError, + QStringLiteral("Failed to extract update archive")); update_in_progress.store(false); return; } @@ -305,7 +345,9 @@ void UpdaterService::OnDownloadFinished() { return; } emit UpdateInstallProgress(100, QStringLiteral("Update completed successfully!")); - emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update installed successfully. Please restart the application.")); + emit UpdateCompleted( + UpdateResult::Success, + QStringLiteral("Update installed successfully. Please restart the application.")); update_in_progress.store(false); CleanupFiles(); }); @@ -344,13 +386,17 @@ void UpdaterService::OnDownloadFinished() { } else { // Create the backup copy of the old AppImage std::string current_version = GetCurrentVersion(); - std::string backup_filename = "citron-backup-" + (current_version.empty() ? "unknown" : current_version) + ".AppImage"; + std::string backup_filename = "citron-backup-" + + (current_version.empty() ? "unknown" : current_version) + + ".AppImage"; std::filesystem::path backup_filepath = backup_dir / backup_filename; - std::filesystem::copy_file(original_appimage_path, backup_filepath, std::filesystem::copy_options::overwrite_existing, ec); + std::filesystem::copy_file(original_appimage_path, backup_filepath, + std::filesystem::copy_options::overwrite_existing, ec); if (ec) { LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message()); } else { - LOG_INFO(Frontend, "Created backup of old AppImage at: {}", backup_filepath.string()); + LOG_INFO(Frontend, "Created backup of old AppImage at: {}", + backup_filepath.string()); } } } @@ -365,9 +411,10 @@ void UpdaterService::OnDownloadFinished() { new_file.write(downloaded_data); new_file.close(); - if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | - QFileDevice::ReadGroup | QFileDevice::ExeGroup | - QFileDevice::ReadOther | QFileDevice::ExeOther)) { + if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ExeOwner | QFileDevice::ReadGroup | + QFileDevice::ExeGroup | QFileDevice::ReadOther | + QFileDevice::ExeOther)) { emit UpdateError(QStringLiteral("Failed to make the new AppImage executable.")); std::filesystem::remove(new_appimage_path, ec); update_in_progress.store(false); @@ -397,7 +444,8 @@ void UpdaterService::OnDownloadFinished() { } LOG_INFO(Frontend, "AppImage updated successfully."); - emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application.")); + emit UpdateCompleted(UpdateResult::Success, + QStringLiteral("Update successful. Please restart the application.")); update_in_progress.store(false); #endif } @@ -430,42 +478,50 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response, const QStri if (channel == QStringLiteral("Stable")) { latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); } else { - latest_version = ExtractCommitHash(release_obj.value(QStringLiteral("name")).toString().toStdString()); + latest_version = ExtractCommitHash( + release_obj.value(QStringLiteral("name")).toString().toStdString()); } - if (latest_version.empty()) continue; + if (latest_version.empty()) + continue; UpdateInfo update_info; update_info.version = latest_version; update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString(); - update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString(); + update_info.release_date = + release_obj.value(QStringLiteral("published_at")).toString().toStdString(); QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray(); for (const QJsonValue& asset_value : assets) { QJsonObject asset_obj = asset_value.toObject(); QString asset_name = asset_obj.value(QStringLiteral("name")).toString(); - #if defined(__linux__) +#if defined(__linux__) if (asset_name.endsWith(QStringLiteral(".AppImage"))) { DownloadOption option; option.name = asset_name.toStdString(); - option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + option.url = asset_obj.value(QStringLiteral("browser_download_url")) + .toString() + .toStdString(); update_info.download_options.push_back(option); } - #elif defined(_WIN32) +#elif defined(_WIN32) // For Windows, find the .zip file but explicitly skip PGO builds. - if (asset_name.endsWith(QStringLiteral(".zip")) && !asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) { + if (asset_name.endsWith(QStringLiteral(".zip")) && + !asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) { DownloadOption option; option.name = asset_name.toStdString(); - option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + option.url = asset_obj.value(QStringLiteral("browser_download_url")) + .toString() + .toStdString(); update_info.download_options.push_back(option); } - #endif - +#endif } if (!update_info.download_options.empty()) { - update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version); + update_info.is_newer_version = + CompareVersions(GetCurrentVersion(), update_info.version); current_update_info = update_info; emit UpdateCheckCompleted(update_info.is_newer_version, update_info); return; @@ -487,28 +543,34 @@ bool UpdaterService::CompareVersions(const std::string& current, const std::stri } #ifdef _WIN32 -bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { +bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path) { #ifdef CITRON_ENABLE_LIBARCHIVE struct archive* a = archive_read_new(); struct archive* ext = archive_write_disk_new(); - if (!a || !ext) return false; + if (!a || !ext) + return false; archive_read_support_format_7zip(a); archive_read_support_filter_all(a); archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); archive_write_disk_set_standard_lookup(ext); - if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) return false; + if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) + return false; EnsureDirectoryExists(extract_path); struct archive_entry* entry; while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { - if (cancel_requested.load()) break; + if (cancel_requested.load()) + break; std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry); archive_entry_set_pathname(entry, entry_path.string().c_str()); - if (archive_write_header(ext, entry) != ARCHIVE_OK) continue; + if (archive_write_header(ext, entry) != ARCHIVE_OK) + continue; const void* buff; size_t size; la_int64_t offset; while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) { - if (cancel_requested.load()) break; + if (cancel_requested.load()) + break; archive_write_data_block(ext, buff, size, offset); } archive_write_finish_entry(ext); @@ -524,12 +586,18 @@ bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, c } #if !defined(CITRON_ENABLE_LIBARCHIVE) -bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { +bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path) { EnsureDirectoryExists(extract_path); - std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; - if (std::system(sevenzip_cmd.c_str()) == 0) return true; - std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + archive_path.string() + "\\\" -DestinationPath \\\"" + extract_path.string() + "\\\" -Force\""; - if (std::system(powershell_cmd.c_str()) == 0) return true; + std::string sevenzip_cmd = + "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; + if (std::system(sevenzip_cmd.c_str()) == 0) + return true; + std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + + archive_path.string() + "\\\" -DestinationPath \\\"" + + extract_path.string() + "\\\" -Force\""; + if (std::system(powershell_cmd.c_str()) == 0) + return true; LOG_ERROR(Frontend, "Failed to extract archive automatically."); return false; } @@ -548,12 +616,15 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { std::filesystem::path staging_path = app_directory / "update_staging"; EnsureDirectoryExists(staging_path); for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) { - if (cancel_requested.load()) return false; + if (cancel_requested.load()) + return false; if (entry.is_regular_file()) { - std::filesystem::path relative_path = std::filesystem::relative(entry.path(), source_path); + std::filesystem::path relative_path = + std::filesystem::relative(entry.path(), source_path); std::filesystem::path staging_dest = staging_path / relative_path; std::filesystem::create_directories(staging_dest.parent_path()); - std::filesystem::copy_file(entry.path(), staging_dest, std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(entry.path(), staging_dest, + std::filesystem::copy_options::overwrite_existing); } } std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; @@ -585,14 +656,16 @@ bool UpdaterService::CreateBackup() { std::filesystem::remove_all(backup_dir); } std::filesystem::create_directories(backup_dir); - std::vector backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", "*.pdb"}; + std::vector backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", + "*.pdb"}; for (const auto& entry : std::filesystem::directory_iterator(app_directory)) { if (entry.is_regular_file()) { std::string filename = entry.path().filename().string(); std::string extension = entry.path().extension().string(); bool should_backup = false; for (const auto& pattern : backup_patterns) { - if (pattern == filename || (pattern.starts_with("*") && pattern.substr(1) == extension)) { + if (pattern == filename || + (pattern.starts_with("*") && pattern.substr(1) == extension)) { should_backup = true; break; } @@ -613,11 +686,13 @@ bool UpdaterService::CreateBackup() { bool UpdaterService::RestoreBackup() { try { std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion()); - if (!std::filesystem::exists(backup_dir)) return false; + if (!std::filesystem::exists(backup_dir)) + return false; for (const auto& entry : std::filesystem::directory_iterator(backup_dir)) { if (entry.is_regular_file()) { std::filesystem::path dest_path = app_directory / entry.path().filename(); - std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(entry.path(), dest_path, + std::filesystem::copy_options::overwrite_existing); } } LOG_INFO(Frontend, "Backup restored successfully"); @@ -648,9 +723,15 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi std::string app_path_str = app_directory.string(); std::string exe_path_str = (app_directory / "citron.exe").string(); - for (auto& ch : staging_path_str) if (ch == '/') ch = '\\'; - for (auto& ch : app_path_str) if (ch == '/') ch = '\\'; - for (auto& ch : exe_path_str) if (ch == '/') ch = '\\'; + for (auto& ch : staging_path_str) + if (ch == '/') + ch = '\\'; + for (auto& ch : app_path_str) + if (ch == '/') + ch = '\\'; + for (auto& ch : exe_path_str) + if (ch == '/') + ch = '\\'; script << "@echo off\n"; script << "setlocal enabledelayedexpansion\n"; @@ -665,7 +746,8 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi script << "if not errorlevel 1 (\n"; script << " set /a wait_count+=1\n"; script << " if !wait_count! gtr 60 (\n"; - script << " echo Warning: Citron process still running after 60 seconds, proceeding anyway...\n"; + script << " echo Warning: Citron process still running after 60 seconds, proceeding " + "anyway...\n"; script << " goto wait_done\n"; script << " )\n"; script << " timeout /t 1 /nobreak >nul\n"; @@ -678,14 +760,17 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi // Remove read-only attributes from all files in the destination directory script << "echo Removing read-only attributes from existing files...\n"; script << "attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n"; - script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str << "\\citron.exe\" >nul 2>&1\n"; - script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str << "\\citron_cmd.exe\" >nul 2>&1\n\n"; + script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str + << "\\citron.exe\" >nul 2>&1\n"; + script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str + << "\\citron_cmd.exe\" >nul 2>&1\n\n"; // Use robocopy for more reliable copying (available on Windows Vista+) script << "echo Copying update files...\n"; script << "set /a copy_retries=0\n"; script << ":copy_loop\n"; - script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str << "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n"; + script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str + << "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n"; script << "set /a robocopy_exit=!errorlevel!\n"; script << "REM Robocopy returns 0-7 for success, 8+ for errors\n"; script << "if !robocopy_exit! geq 8 (\n"; @@ -786,10 +871,13 @@ bool UpdaterService::LaunchUpdateHelper() { bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments); if (launched) { - LOG_INFO(Frontend, "Update helper script launched successfully from: {}", script_path.string()); + LOG_INFO(Frontend, "Update helper script launched successfully from: {}", + script_path.string()); return true; } else { - LOG_ERROR(Frontend, "Failed to launch update helper script. QProcess::startDetached returned false"); + LOG_ERROR( + Frontend, + "Failed to launch update helper script. QProcess::startDetached returned false"); return false; } } catch (const std::exception& e) { @@ -808,14 +896,16 @@ bool UpdaterService::CleanupFiles() { std::vector backup_dirs; if (std::filesystem::exists(backup_path)) { for (const auto& entry : std::filesystem::directory_iterator(backup_path)) { - if (entry.is_directory() && entry.path().filename().string().starts_with("backup_")) { + if (entry.is_directory() && + entry.path().filename().string().starts_with("backup_")) { backup_dirs.push_back(entry.path()); } } } if (backup_dirs.size() > 3) { - std::sort(backup_dirs.begin(), backup_dirs.end(), - [](const auto& a, const auto& b) { return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); }); + std::sort(backup_dirs.begin(), backup_dirs.end(), [](const auto& a, const auto& b) { + return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); + }); for (size_t i = 3; i < backup_dirs.size(); ++i) { std::filesystem::remove_all(backup_dirs[i]); } @@ -829,7 +919,9 @@ bool UpdaterService::CleanupFiles() { } std::filesystem::path UpdaterService::GetTempDirectory() const { - return std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater"; + return std::filesystem::path( + QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / + "citron_updater"; } std::filesystem::path UpdaterService::GetApplicationDirectory() const { @@ -856,7 +948,8 @@ bool UpdaterService::HasStagedUpdate(const std::filesystem::path& app_directory) #ifdef _WIN32 std::filesystem::path staging_path = app_directory / "update_staging"; std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; - return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) && std::filesystem::is_directory(staging_path); + return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) && + std::filesystem::is_directory(staging_path); #else return false; #endif @@ -867,15 +960,19 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director try { std::filesystem::path staging_path = app_directory / "update_staging"; std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; - if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file)) return false; + if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file)) + return false; LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string()); std::filesystem::path backup_path_dir = app_directory / "backup_before_update"; - if (std::filesystem::exists(backup_path_dir)) std::filesystem::remove_all(backup_path_dir); + if (std::filesystem::exists(backup_path_dir)) + std::filesystem::remove_all(backup_path_dir); std::filesystem::create_directories(backup_path_dir); for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) { - if (entry.path().filename() == "update_manifest.txt") continue; + if (entry.path().filename() == "update_manifest.txt") + continue; if (entry.is_regular_file()) { - std::filesystem::path relative_path = std::filesystem::relative(entry.path(), staging_path); + std::filesystem::path relative_path = + std::filesystem::relative(entry.path(), staging_path); std::filesystem::path dest_path = app_directory / relative_path; if (std::filesystem::exists(dest_path)) { std::filesystem::path backup_dest = backup_path_dir / relative_path; @@ -883,7 +980,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director std::filesystem::copy_file(dest_path, backup_dest); } std::filesystem::create_directories(dest_path.parent_path()); - std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(entry.path(), dest_path, + std::filesystem::copy_options::overwrite_existing); } } std::ifstream manifest(manifest_file); @@ -897,7 +995,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director if (!version.empty()) { std::filesystem::path version_file = app_directory / "version.txt"; std::ofstream vfile(version_file); - if (vfile.is_open()) vfile << version; + if (vfile.is_open()) + vfile << version; } std::filesystem::remove_all(staging_path); LOG_INFO(Frontend, "Update applied successfully. Version: {}", version); diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index bfa18aa90..96ab8870a 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -3,21 +3,24 @@ #pragma once -#include -#include #include -#include #include +#include #include +#include +#include + -#include #include +#include + namespace Updater { // Declarations for helper functions QString FormatDateTimeString(const std::string& iso_string); std::string ExtractCommitHash(const std::string& version_string); +std::string ExtractVersionTag(const std::string& version_string); QByteArray GetFileChecksum(const std::filesystem::path& file_path); struct DownloadOption { @@ -39,7 +42,16 @@ class UpdaterService : public QObject { Q_OBJECT public: - enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable }; + enum class UpdateResult { + Success, + Failed, + Cancelled, + NetworkError, + ExtractionError, + PermissionError, + InvalidArchive, + NoUpdateAvailable + }; explicit UpdaterService(QObject* parent = nullptr); ~UpdaterService() override; @@ -53,9 +65,9 @@ public: static bool HasStagedUpdate(const std::filesystem::path& app_directory); static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); - #ifdef _WIN32 +#ifdef _WIN32 bool LaunchUpdateHelper(); - #endif +#endif signals: void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); @@ -75,16 +87,18 @@ private: void ParseUpdateResponse(const QByteArray& response, const QString& channel); bool CompareVersions(const std::string& current, const std::string& latest) const; - #ifdef _WIN32 - bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); - #ifndef CITRON_ENABLE_LIBARCHIVE - bool ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); - #endif +#ifdef _WIN32 + bool ExtractArchive(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path); +#ifndef CITRON_ENABLE_LIBARCHIVE + bool ExtractArchiveWindows(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path); +#endif bool InstallUpdate(const std::filesystem::path& update_path); bool CreateBackup(); bool RestoreBackup(); bool CreateUpdateHelperScript(const std::filesystem::path& staging_path); - #endif +#endif bool CleanupFiles(); std::filesystem::path GetTempDirectory() const; std::filesystem::path GetApplicationDirectory() const; From bc3cd4c639d52efd55118e8a1ccef34881cf57ae Mon Sep 17 00:00:00 2001 From: Collecting Date: Thu, 5 Feb 2026 02:25:38 +0100 Subject: [PATCH 14/16] Fix MSVC & macOS compiler issues Signed-off-by: Collecting --- src/citron/configuration/configure_hotkeys.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/citron/configuration/configure_hotkeys.cpp b/src/citron/configuration/configure_hotkeys.cpp index 8f6a22d65..ac108df55 100644 --- a/src/citron/configuration/configure_hotkeys.cpp +++ b/src/citron/configuration/configure_hotkeys.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include #include #include From 43326b89e7e065e6326ab71346f262d2ce07ab7b Mon Sep 17 00:00:00 2001 From: Collecting Date: Thu, 5 Feb 2026 02:53:16 +0100 Subject: [PATCH 15/16] Windows Header Ordering Signed-off-by: Collecting --- src/citron/updater/updater_service.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index 1b737cb2a..d1ee33f84 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -35,9 +35,8 @@ #include #ifdef _WIN32 -#include #include - +#include #endif namespace Updater { From 94437370347b88cd529d19e5afff7aed1b5087ed Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 21:59:51 -0500 Subject: [PATCH 16/16] fix: Icon Blurriness & add Citron Logo --- src/citron/citron.qrc | 1 + src/citron/game_list.cpp | 77 ++++++++++++++++++++++++++++++++-------- src/citron/game_list_p.h | 14 +++++--- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/citron/citron.qrc b/src/citron/citron.qrc index a85fb4575..4f5ed3919 100644 --- a/src/citron/citron.qrc +++ b/src/citron/citron.qrc @@ -10,5 +10,6 @@ SPDX-License-Identifier: GPL-2.0-or-later ../../dist/dice.svg + ../../dist/citron.svg diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 9d06fbd79..bd13eae1a 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -1187,7 +1187,13 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) { QPixmap icon; if (original_item) { - icon = original_item->data(Qt::DecorationRole).value(); + icon = original_item->data(GameListItemPath::HighResIconRole).value(); + if (icon.isNull()) { + icon = original_item->data(Qt::DecorationRole).value(); + } else { + // Apply rounded corners to the high-res icon + icon = CreateRoundIcon(icon, 256); + } } else { // Fallback for safety icon = item.data(Qt::DecorationRole).value(); @@ -1247,33 +1253,76 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) { zoom_anim->setEasingCurve(QEasingCurve::OutCubic); auto* fly_fade_group = new QParallelAnimationGroup; - auto* effect = new QGraphicsOpacityEffect(animation_label); - animation_label->setGraphicsEffect(effect); + auto* icon_effect = new QGraphicsOpacityEffect(animation_label); + animation_label->setGraphicsEffect(icon_effect); auto* fly_anim = new QPropertyAnimation(animation_label, "geometry"); fly_anim->setDuration(350); fly_anim->setStartValue(zoom_end_geom); fly_anim->setEndValue(fly_end_geom); fly_anim->setEasingCurve(QEasingCurve::InQuad); - auto* fade_anim = new QPropertyAnimation(effect, "opacity"); - fade_anim->setDuration(350); - fade_anim->setStartValue(1.0f); - fade_anim->setEndValue(0.0f); - fade_anim->setEasingCurve(QEasingCurve::InQuad); + auto* icon_fade_anim = new QPropertyAnimation(icon_effect, "opacity"); + icon_fade_anim->setDuration(350); + icon_fade_anim->setStartValue(1.0f); + icon_fade_anim->setEndValue(0.0f); + icon_fade_anim->setEasingCurve(QEasingCurve::InQuad); fly_fade_group->addAnimation(fly_anim); - fly_fade_group->addAnimation(fade_anim); + fly_fade_group->addAnimation(icon_fade_anim); - auto* main_group = new QSequentialAnimationGroup(animation_label); + // --- 4. CITRON LOGO TRANSITION --- + auto* logo_label = new QLabel(main_window); + QPixmap logo_pixmap(QStringLiteral(":/citron.svg")); + logo_label->setPixmap( + logo_pixmap.scaled(400, 400, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + logo_label->setFixedSize(400, 400); + logo_label->move(center_point.x() - 200, center_point.y() - 200); + logo_label->hide(); + + auto* logo_effect = new QGraphicsOpacityEffect(logo_label); + logo_label->setGraphicsEffect(logo_effect); + logo_effect->setOpacity(0.0f); + + auto* logo_fade_in = new QPropertyAnimation(logo_effect, "opacity"); + logo_fade_in->setDuration(500); + logo_fade_in->setStartValue(0.0f); + logo_fade_in->setEndValue(1.0f); + logo_fade_in->setEasingCurve(QEasingCurve::InOutQuad); + + auto* logo_fade_out = new QPropertyAnimation(logo_effect, "opacity"); + logo_fade_out->setDuration(500); + logo_fade_out->setStartValue(1.0f); + logo_fade_out->setEndValue(0.0f); + logo_fade_out->setEasingCurve(QEasingCurve::InOutQuad); + + // Overlap the icon "fly-away" and the logo "fade-in" + auto* overlap_group = new QParallelAnimationGroup; + overlap_group->addAnimation(fly_fade_group); + + auto* logo_fade_in_seq = new QSequentialAnimationGroup; + logo_fade_in_seq->addPause(100); // 100ms delay so it starts mid-fly + logo_fade_in_seq->addAnimation(logo_fade_in); + overlap_group->addAnimation(logo_fade_in_seq); + + auto* main_group = new QSequentialAnimationGroup(this); main_group->addAnimation(zoom_anim); main_group->addPause(50); - main_group->addAnimation(fly_fade_group); - // When the icon animation finishes, launch the game and clean up. - // The black overlay will remain until OnEmulationEnded is called. + // Show logo once zoom is finished, just before fly/fade starts + connect(zoom_anim, &QPropertyAnimation::finished, [logo_label]() { + logo_label->show(); + logo_label->raise(); + }); + + main_group->addAnimation(overlap_group); + main_group->addPause(1000); // Shorter 1 second pause + main_group->addAnimation(logo_fade_out); + + // When the animation finishes, launch the game and clean up. connect(main_group, &QSequentialAnimationGroup::finished, this, - [this, file_path, title_id, animation_label]() { + [this, file_path, title_id, animation_label, logo_label]() { search_field->clear(); emit GameChosen(file_path, title_id); animation_label->deleteLater(); + logo_label->deleteLater(); }); main_group->start(QAbstractAnimation::DeleteWhenStopped); diff --git a/src/citron/game_list_p.h b/src/citron/game_list_p.h index 85a428d9e..dbbebdb1b 100644 --- a/src/citron/game_list_p.h +++ b/src/citron/game_list_p.h @@ -18,12 +18,13 @@ #include #include -#include "common/common_types.h" -#include "common/logging/log.h" -#include "common/string_util.h" #include "citron/play_time_manager.h" #include "citron/uisettings.h" #include "citron/util/util.h" +#include "common/common_types.h" +#include "common/logging/log.h" +#include "common/string_util.h" + enum class GameListItemType { Game = QStandardItem::UserType + 1, @@ -62,7 +63,8 @@ static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) { painter.setRenderHint(QPainter::Antialiasing); // Create a rounded rectangle clipping path - const int radius = size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding) + const int radius = + size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding) QPainterPath path; path.addRoundedRect(0, 0, size, size, radius, radius); painter.setClipPath(path); @@ -98,6 +100,7 @@ public: static constexpr int FullPathRole = SortRole + 2; static constexpr int ProgramIdRole = SortRole + 3; static constexpr int FileTypeRole = SortRole + 4; + static constexpr int HighResIconRole = SortRole + 5; GameListItemPath() = default; GameListItemPath(const QString& game_path, const std::vector& picture_data, @@ -115,6 +118,9 @@ public: picture = GetDefaultIcon(size); } + // Store unscaled pixmap for high-quality animations + setData(picture, HighResIconRole); + // Create a round icon QPixmap round_picture = CreateRoundIcon(picture, size);