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..b6c61d715 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -790,7 +790,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 +1035,5 @@ std::vector ManualContentProvider::ListEntriesFilter( out.erase(std::unique(out.begin(), out.end()), out.end()); return out; } + } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index ffcc9ea18..5b56c67c5 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); @@ -209,7 +209,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..bd1b430eb 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -5,6 +5,8 @@ #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/applet_data_broker.h" #include "core/hle/service/am/applet_manager.h" @@ -12,10 +14,13 @@ #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 { @@ -262,6 +267,25 @@ 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); @@ -274,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 62a797056..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 { @@ -46,6 +47,10 @@ public: void OperationModeChanged(); public: + WindowSystem* GetWindowSystem() const { + return m_window_system; + } + void SetWindowSystem(WindowSystem* window_system); void SetHomeMenuRequestCallback(std::function callback); @@ -59,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/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/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/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/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/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/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 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,