mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-26 03:19:41 -04:00
Merge branch 'main' into feature/project-thor-optimizations
This commit is contained in:
@@ -178,6 +178,8 @@ add_executable(citron
|
||||
game_list_worker.h
|
||||
hotkeys.cpp
|
||||
hotkeys.h
|
||||
hotkey_profile_manager.cpp
|
||||
hotkey_profile_manager.h
|
||||
install_dialog.cpp
|
||||
install_dialog.h
|
||||
loading_screen.cpp
|
||||
|
||||
@@ -10,5 +10,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</qresource>
|
||||
<qresource prefix="/">
|
||||
<file alias="dist/dice.svg">../../dist/dice.svg</file>
|
||||
<file alias="citron.svg">../../dist/citron.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "citron/configuration/configure_dialog.h"
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <QApplication>
|
||||
@@ -20,17 +19,12 @@
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/settings_enums.h"
|
||||
#include "core/core.h"
|
||||
#include "ui_configure.h"
|
||||
#include "vk_device_info.h"
|
||||
#include "citron/configuration/configuration_shared.h"
|
||||
#include "citron/configuration/configure_applets.h"
|
||||
#include "citron/configuration/configure_audio.h"
|
||||
#include "citron/configuration/configure_cpu.h"
|
||||
#include "citron/configuration/configure_debug_tab.h"
|
||||
#include "citron/configuration/configure_dialog.h"
|
||||
#include "citron/configuration/configure_filesystem.h"
|
||||
#include "citron/configuration/configure_general.h"
|
||||
#include "citron/configuration/configure_graphics.h"
|
||||
@@ -44,12 +38,18 @@
|
||||
#include "citron/configuration/configure_ui.h"
|
||||
#include "citron/configuration/configure_web.h"
|
||||
#include "citron/configuration/style_animation_event_filter.h"
|
||||
#include "citron/util/rainbow_style.h"
|
||||
#include "citron/game_list.h"
|
||||
#include "citron/hotkeys.h"
|
||||
#include "citron/main.h"
|
||||
#include "citron/theme.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "citron/util/rainbow_style.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/settings_enums.h"
|
||||
#include "core/core.h"
|
||||
#include "ui_configure.h"
|
||||
#include "vk_device_info.h"
|
||||
|
||||
static QScrollArea* CreateScrollArea(QWidget* widget) {
|
||||
auto* scroll_area = new QScrollArea();
|
||||
@@ -95,7 +95,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
ui_tab->UpdateScreenshotInfo(ratio, setup);
|
||||
},
|
||||
nullptr, *builder, this)},
|
||||
hotkeys_tab{std::make_unique<ConfigureHotkeys>(system_.HIDCore(), this)},
|
||||
hotkeys_tab{std::make_unique<ConfigureHotkeys>(registry, system_.HIDCore(), this)},
|
||||
input_tab{std::make_unique<ConfigureInput>(system_, this)},
|
||||
network_tab{std::make_unique<ConfigureNetwork>(system_, this)},
|
||||
profile_tab{std::make_unique<ConfigureProfileManager>(system_, this)},
|
||||
@@ -103,8 +103,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
web_tab{std::make_unique<ConfigureWeb>(this)} {
|
||||
|
||||
if (auto* main_window = qobject_cast<GMainWindow*>(parent)) {
|
||||
connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh,
|
||||
main_window, &GMainWindow::RefreshGameList);
|
||||
connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh, main_window,
|
||||
&GMainWindow::RefreshGameList);
|
||||
}
|
||||
|
||||
Settings::SetConfiguringGlobal(true);
|
||||
@@ -115,7 +115,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
|
||||
setWindowModality(Qt::NonModal);
|
||||
} else {
|
||||
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
|
||||
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
|
||||
Qt::WindowCloseButtonHint);
|
||||
setWindowModality(Qt::WindowModal);
|
||||
}
|
||||
|
||||
@@ -180,18 +181,21 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
ui->stackedWidget->addWidget(CreateScrollArea(applets_tab.get()));
|
||||
ui->stackedWidget->addWidget(CreateScrollArea(debug_tab_tab.get()));
|
||||
|
||||
connect(tab_button_group.get(), qOverload<int>(&QButtonGroup::idClicked), this, &ConfigureDialog::AnimateTabSwitch);
|
||||
connect(tab_button_group.get(), qOverload<int>(&QButtonGroup::idClicked), this,
|
||||
&ConfigureDialog::AnimateTabSwitch);
|
||||
connect(ui_tab.get(), &ConfigureUi::themeChanged, this, &ConfigureDialog::UpdateTheme);
|
||||
connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this, &ConfigureDialog::SetUIPositioning);
|
||||
connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this,
|
||||
&ConfigureDialog::SetUIPositioning);
|
||||
web_tab->SetWebServiceConfigEnabled(enable_web_config);
|
||||
hotkeys_tab->Populate(registry);
|
||||
hotkeys_tab->Populate();
|
||||
input_tab->Initialize(input_subsystem);
|
||||
general_tab->SetResetCallback([&] { this->close(); });
|
||||
SetConfiguration();
|
||||
connect(ui_tab.get(), &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged);
|
||||
if (system.IsPoweredOn()) {
|
||||
if (auto* apply_button = ui->buttonBox->button(QDialogButtonBox::Apply)) {
|
||||
connect(apply_button, &QAbstractButton::clicked, this, &ConfigureDialog::HandleApplyButtonClicked);
|
||||
connect(apply_button, &QAbstractButton::clicked, this,
|
||||
&ConfigureDialog::HandleApplyButtonClicked);
|
||||
}
|
||||
}
|
||||
ui->stackedWidget->setCurrentIndex(0);
|
||||
@@ -219,10 +223,12 @@ void ConfigureDialog::UpdateTheme() {
|
||||
const QString d_txt = is_dark ? QStringLiteral("#8d8d8d") : QStringLiteral("#a0a0a0");
|
||||
|
||||
// Use dark shadow on light backgrounds, light shadow on dark backgrounds
|
||||
const QString shadow_color = is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)");
|
||||
const QString shadow_color =
|
||||
is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)");
|
||||
|
||||
static QString cached_template;
|
||||
if (cached_template.isEmpty()) cached_template = property("templateStyleSheet").toString();
|
||||
if (cached_template.isEmpty())
|
||||
cached_template = property("templateStyleSheet").toString();
|
||||
QString style_sheet = cached_template;
|
||||
|
||||
style_sheet.replace(QStringLiteral("%%ACCENT_COLOR%%"), accent);
|
||||
@@ -237,10 +243,10 @@ void ConfigureDialog::UpdateTheme() {
|
||||
style_sheet.replace(QStringLiteral("%%FOCUS_BG_COLOR%%"), f_bg);
|
||||
style_sheet.replace(QStringLiteral("%%DISABLED_TEXT_COLOR%%"), d_txt);
|
||||
|
||||
style_sheet += QStringLiteral(
|
||||
"QSlider::handle:horizontal { background-color: %1; }"
|
||||
"QCheckBox::indicator:checked { background-color: %1; border-color: %1; }"
|
||||
).arg(accent);
|
||||
style_sheet +=
|
||||
QStringLiteral("QSlider::handle:horizontal { background-color: %1; }"
|
||||
"QCheckBox::indicator:checked { background-color: %1; border-color: %1; }")
|
||||
.arg(accent);
|
||||
|
||||
setStyleSheet(style_sheet);
|
||||
|
||||
@@ -250,33 +256,37 @@ void ConfigureDialog::UpdateTheme() {
|
||||
cpu_tab->SetTemplateStyleSheet(style_sheet);
|
||||
graphics_advanced_tab->SetTemplateStyleSheet(style_sheet);
|
||||
|
||||
QString sidebar_css = QStringLiteral(
|
||||
"QPushButton.tabButton { "
|
||||
QString sidebar_css =
|
||||
QStringLiteral(
|
||||
"QPushButton.tabButton { "
|
||||
"background-color: %1; "
|
||||
"color: %2; "
|
||||
"border: 2px solid transparent; "
|
||||
"}"
|
||||
"QPushButton.tabButton:checked { "
|
||||
"color: %4; " // Use main text color instead of dimmed color for checked state
|
||||
"}"
|
||||
"QPushButton.tabButton:checked { "
|
||||
"color: %4; " // Use main text color instead of dimmed color for checked state
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:hover { "
|
||||
"}"
|
||||
"QPushButton.tabButton:hover { "
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:pressed { "
|
||||
"}"
|
||||
"QPushButton.tabButton:pressed { "
|
||||
"background-color: %3; "
|
||||
"color: #ffffff; "
|
||||
"}"
|
||||
).arg(b_bg, d_txt, accent, txt);
|
||||
"}")
|
||||
.arg(b_bg, d_txt, accent, txt);
|
||||
|
||||
if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(sidebar_css);
|
||||
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css);
|
||||
if (ui->topButtonWidget)
|
||||
ui->topButtonWidget->setStyleSheet(sidebar_css);
|
||||
if (ui->horizontalNavWidget)
|
||||
ui->horizontalNavWidget->setStyleSheet(sidebar_css);
|
||||
|
||||
if (is_rainbow) {
|
||||
if (!rainbow_timer) {
|
||||
rainbow_timer = new QTimer(this);
|
||||
connect(rainbow_timer, &QTimer::timeout, this, [this, b_bg, d_txt, txt, shadow_color] {
|
||||
if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) {
|
||||
if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() ||
|
||||
!this->isActiveWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -288,43 +298,52 @@ void ConfigureDialog::UpdateTheme() {
|
||||
const QString hue_light = current_color.lighter(125).name();
|
||||
const QString hue_dark = current_color.darker(150).name();
|
||||
|
||||
QString rainbow_sidebar_css = QStringLiteral(
|
||||
"QPushButton.tabButton { "
|
||||
"background-color: %1; "
|
||||
"color: %2; "
|
||||
"border: 2px solid transparent; "
|
||||
"}"
|
||||
"QPushButton.tabButton:checked { "
|
||||
"color: %4; " // Use main text color for visibility
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:hover { "
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:pressed { "
|
||||
"background-color: %3; "
|
||||
"color: #ffffff; "
|
||||
"}"
|
||||
).arg(b_bg, d_txt, hue_hex, txt);
|
||||
QString rainbow_sidebar_css =
|
||||
QStringLiteral("QPushButton.tabButton { "
|
||||
"background-color: %1; "
|
||||
"color: %2; "
|
||||
"border: 2px solid transparent; "
|
||||
"}"
|
||||
"QPushButton.tabButton:checked { "
|
||||
"color: %4; " // Use main text color for visibility
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:hover { "
|
||||
"border: 2px solid %3; "
|
||||
"}"
|
||||
"QPushButton.tabButton:pressed { "
|
||||
"background-color: %3; "
|
||||
"color: #ffffff; "
|
||||
"}")
|
||||
.arg(b_bg, d_txt, hue_hex, txt);
|
||||
|
||||
if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css);
|
||||
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css);
|
||||
if (ui->topButtonWidget)
|
||||
ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css);
|
||||
if (ui->horizontalNavWidget)
|
||||
ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css);
|
||||
|
||||
// Tab Content Area
|
||||
if (current_index == input_tab_index) return;
|
||||
if (current_index == input_tab_index)
|
||||
return;
|
||||
|
||||
QWidget* currentContainer = ui->stackedWidget->currentWidget();
|
||||
if (currentContainer) {
|
||||
QString tab_css = QStringLiteral(
|
||||
"QCheckBox::indicator:checked, QRadioButton::indicator:checked { background-color: %1; border: 1px solid %1; }"
|
||||
"QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }"
|
||||
"QSlider::handle:horizontal { background-color: %1; border: 1px solid %1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }"
|
||||
"QPushButton, QToolButton { background-color: transparent; color: %4; border: 2px solid %1; border-radius: 4px; padding: 5px; }"
|
||||
"QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }"
|
||||
"QPushButton:pressed, QToolButton:pressed { background-color: %3; color: #ffffff; border-color: %3; }"
|
||||
).arg(hue_hex, hue_light, hue_dark, txt);
|
||||
QString tab_css =
|
||||
QStringLiteral(
|
||||
"QCheckBox::indicator:checked, QRadioButton::indicator:checked { "
|
||||
"background-color: %1; border: 1px solid %1; }"
|
||||
"QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }"
|
||||
"QSlider::handle:horizontal { background-color: %1; border: 1px solid "
|
||||
"%1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }"
|
||||
"QPushButton, QToolButton { background-color: transparent; color: %4; "
|
||||
"border: 2px solid %1; border-radius: 4px; padding: 5px; }"
|
||||
"QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }"
|
||||
"QPushButton:pressed, QToolButton:pressed { background-color: %3; "
|
||||
"color: #ffffff; border-color: %3; }")
|
||||
.arg(hue_hex, hue_light, hue_dark, txt);
|
||||
currentContainer->setStyleSheet(tab_css);
|
||||
if (ui->buttonBox) ui->buttonBox->setStyleSheet(tab_css);
|
||||
if (ui->buttonBox)
|
||||
ui->buttonBox->setStyleSheet(tab_css);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -334,7 +353,8 @@ void ConfigureDialog::UpdateTheme() {
|
||||
if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) {
|
||||
rainbow_timer->stop();
|
||||
|
||||
if (ui->buttonBox) ui->buttonBox->setStyleSheet({});
|
||||
if (ui->buttonBox)
|
||||
ui->buttonBox->setStyleSheet({});
|
||||
for (int i = 0; i < ui->stackedWidget->count(); ++i) {
|
||||
if (auto* w = ui->stackedWidget->widget(i)) {
|
||||
w->setStyleSheet({});
|
||||
@@ -370,7 +390,8 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) {
|
||||
|
||||
if (!tab_buttons.empty()) {
|
||||
const int button_height = tab_buttons[0]->sizeHint().height();
|
||||
const int margins = h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom();
|
||||
const int margins =
|
||||
h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom();
|
||||
// The scroll area frame adds a few pixels, this accounts for it.
|
||||
const int fixed_height = button_height + margins + 4;
|
||||
ui->horizontalNavScrollArea->setMaximumHeight(fixed_height);
|
||||
@@ -417,7 +438,7 @@ void ConfigureDialog::ApplyConfiguration() {
|
||||
profile_tab->ApplyConfiguration();
|
||||
filesystem_tab->ApplyConfiguration();
|
||||
input_tab->ApplyConfiguration();
|
||||
hotkeys_tab->ApplyConfiguration(registry);
|
||||
hotkeys_tab->ApplyConfiguration();
|
||||
cpu_tab->ApplyConfiguration();
|
||||
graphics_tab->ApplyConfiguration();
|
||||
graphics_advanced_tab->ApplyConfiguration();
|
||||
@@ -501,7 +522,8 @@ void ConfigureDialog::AnimateTabSwitch(int id) {
|
||||
anim_new_opacity->setDuration(duration);
|
||||
anim_new_opacity->setEasingCurve(QEasingCurve::InQuad);
|
||||
|
||||
auto* button_opacity_effect = qobject_cast<QGraphicsOpacityEffect*>(ui->buttonBox->graphicsEffect());
|
||||
auto* button_opacity_effect =
|
||||
qobject_cast<QGraphicsOpacityEffect*>(ui->buttonBox->graphicsEffect());
|
||||
if (!button_opacity_effect) {
|
||||
button_opacity_effect = new QGraphicsOpacityEffect(ui->buttonBox);
|
||||
ui->buttonBox->setGraphicsEffect(button_opacity_effect);
|
||||
@@ -529,18 +551,19 @@ void ConfigureDialog::AnimateTabSwitch(int id) {
|
||||
animation_group->addAnimation(anim_new_opacity);
|
||||
animation_group->addAnimation(button_anim_sequence);
|
||||
|
||||
connect(animation_group, &QAbstractAnimation::finished, this, [this, current_widget, next_widget, id]() {
|
||||
ui->stackedWidget->setCurrentIndex(id);
|
||||
connect(animation_group, &QAbstractAnimation::finished, this,
|
||||
[this, current_widget, next_widget, id]() {
|
||||
ui->stackedWidget->setCurrentIndex(id);
|
||||
|
||||
next_widget->setGraphicsEffect(nullptr);
|
||||
current_widget->hide();
|
||||
current_widget->move(0, 0);
|
||||
next_widget->setGraphicsEffect(nullptr);
|
||||
current_widget->hide();
|
||||
current_widget->move(0, 0);
|
||||
|
||||
m_is_tab_animating = false; // Reset the flag
|
||||
for (auto button : tab_button_group->buttons()) {
|
||||
button->setEnabled(true);
|
||||
}
|
||||
});
|
||||
m_is_tab_animating = false; // Reset the flag
|
||||
for (auto button : tab_button_group->buttons()) {
|
||||
button->setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
m_is_tab_animating = true; // Set the flag
|
||||
for (auto button : tab_button_group->buttons()) {
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-FileCopyrightText: 2026 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QModelIndex>
|
||||
#include <QShortcut>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTimer>
|
||||
|
||||
#include "hid_core/frontend/emulated_controller.h"
|
||||
#include "hid_core/hid_core.h"
|
||||
|
||||
#include "frontend_common/config.h"
|
||||
#include "ui_configure_hotkeys.h"
|
||||
#include "citron/configuration/configure_hotkeys.h"
|
||||
#include "citron/hotkeys.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "citron/util/sequence_dialog/sequence_dialog.h"
|
||||
#include "frontend_common/config.h"
|
||||
#include "ui_configure_hotkeys.h"
|
||||
|
||||
constexpr int name_column = 0;
|
||||
constexpr int hotkey_column = 1;
|
||||
constexpr int controller_column = 2;
|
||||
|
||||
ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent)
|
||||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureHotkeys>()),
|
||||
ConfigureHotkeys::ConfigureHotkeys(HotkeyRegistry& registry_, Core::HID::HIDCore& hid_core,
|
||||
QWidget* parent)
|
||||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureHotkeys>()), registry(registry_),
|
||||
controller(new Core::HID::EmulatedController(Core::HID::NpadIdType::Player1)),
|
||||
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
|
||||
ui->setupUi(this);
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
@@ -36,14 +46,28 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
|
||||
ui->hotkey_list->setModel(model);
|
||||
|
||||
ui->hotkey_list->header()->setStretchLastSection(false);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::ResizeMode::Stretch);
|
||||
ui->hotkey_list->header()->setMinimumSectionSize(150);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch);
|
||||
ui->hotkey_list->header()->setMinimumSectionSize(70);
|
||||
|
||||
connect(ui->button_restore_defaults, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::RestoreDefaults);
|
||||
connect(ui->button_clear_all, &QPushButton::clicked, this, &ConfigureHotkeys::ClearAll);
|
||||
|
||||
controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
||||
// Profile Management Connections
|
||||
connect(ui->button_new_profile, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::OnCreateProfile);
|
||||
connect(ui->button_delete_profile, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::OnDeleteProfile);
|
||||
connect(ui->button_rename_profile, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::OnRenameProfile);
|
||||
connect(ui->button_import_profile, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::OnImportProfile);
|
||||
connect(ui->button_export_profile, &QPushButton::clicked, this,
|
||||
&ConfigureHotkeys::OnExportProfile);
|
||||
connect(ui->combo_box_profile, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&ConfigureHotkeys::OnProfileChanged);
|
||||
|
||||
connect(timeout_timer.get(), &QTimer::timeout, [this] {
|
||||
const bool is_button_pressed = pressed_buttons != Core::HID::NpadButton::None ||
|
||||
@@ -53,8 +77,8 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
|
||||
|
||||
connect(poll_timer.get(), &QTimer::timeout, [this] {
|
||||
pressed_buttons |= controller->GetNpadButtons().raw;
|
||||
pressed_home_button |= this->controller->GetHomeButtons().home != 0;
|
||||
pressed_capture_button |= this->controller->GetCaptureButtons().capture != 0;
|
||||
pressed_home_button |= controller->GetHomeButtons().home != 0;
|
||||
pressed_capture_button |= controller->GetCaptureButtons().capture != 0;
|
||||
if (pressed_buttons != Core::HID::NpadButton::None || pressed_home_button ||
|
||||
pressed_capture_button) {
|
||||
const QString button_name =
|
||||
@@ -64,38 +88,230 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
|
||||
model->setData(button_model_index, button_name);
|
||||
}
|
||||
});
|
||||
RetranslateUI();
|
||||
|
||||
ui->hotkey_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
// Populate profile list first
|
||||
UpdateProfileList();
|
||||
}
|
||||
|
||||
ConfigureHotkeys::~ConfigureHotkeys() = default;
|
||||
|
||||
void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) {
|
||||
for (const auto& group : registry.hotkey_groups) {
|
||||
QString parent_item_data = QString::fromStdString(group.first);
|
||||
auto* parent_item =
|
||||
new QStandardItem(QCoreApplication::translate("Hotkeys", qPrintable(parent_item_data)));
|
||||
parent_item->setEditable(false);
|
||||
parent_item->setData(parent_item_data);
|
||||
for (const auto& hotkey : group.second) {
|
||||
QString hotkey_action_data = QString::fromStdString(hotkey.first);
|
||||
auto* action = new QStandardItem(
|
||||
QCoreApplication::translate("Hotkeys", qPrintable(hotkey_action_data)));
|
||||
auto* keyseq =
|
||||
new QStandardItem(hotkey.second.keyseq.toString(QKeySequence::NativeText));
|
||||
auto* controller_keyseq =
|
||||
new QStandardItem(QString::fromStdString(hotkey.second.controller_keyseq));
|
||||
action->setEditable(false);
|
||||
action->setData(hotkey_action_data);
|
||||
keyseq->setEditable(false);
|
||||
controller_keyseq->setEditable(false);
|
||||
parent_item->appendRow({action, keyseq, controller_keyseq});
|
||||
}
|
||||
model->appendRow(parent_item);
|
||||
void ConfigureHotkeys::Populate(const std::string& profile_name) {
|
||||
const auto& profiles = profile_manager.GetProfiles();
|
||||
std::string target_profile = profile_name;
|
||||
if (target_profile.empty()) {
|
||||
target_profile = ui->combo_box_profile->currentText().toStdString();
|
||||
}
|
||||
if (target_profile.empty()) {
|
||||
target_profile = profiles.current_profile;
|
||||
}
|
||||
|
||||
// Use default if current profile missing (safety)
|
||||
std::vector<Hotkey::BackendShortcut> current_shortcuts;
|
||||
if (profiles.profiles.count(target_profile)) {
|
||||
current_shortcuts = profiles.profiles.at(target_profile);
|
||||
} else if (profiles.profiles.count("Default")) {
|
||||
current_shortcuts = profiles.profiles.at("Default");
|
||||
}
|
||||
|
||||
// Map overrides for easy lookup: Key = Group + Name
|
||||
std::map<std::pair<std::string, std::string>, Hotkey::BackendShortcut> overrides;
|
||||
for (const auto& s : current_shortcuts) {
|
||||
overrides[{s.group, s.name}] = s;
|
||||
}
|
||||
|
||||
model->clear();
|
||||
model->setColumnCount(3);
|
||||
model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")});
|
||||
|
||||
for (const auto& [group_name, group_map] : registry.hotkey_groups) {
|
||||
auto* parent_item = new QStandardItem(
|
||||
QCoreApplication::translate("Hotkeys", qPrintable(QString::fromStdString(group_name))));
|
||||
parent_item->setEditable(false);
|
||||
parent_item->setData(QString::fromStdString(group_name), Qt::UserRole);
|
||||
model->appendRow(parent_item);
|
||||
|
||||
for (const auto& [action_name, hotkey] : group_map) {
|
||||
// Determine values (Registry Default vs Profile Override)
|
||||
QString keyseq_str = hotkey.keyseq.toString(QKeySequence::NativeText);
|
||||
QString portable_keyseq = hotkey.keyseq.toString(QKeySequence::PortableText);
|
||||
QString controller_keyseq_str = QString::fromStdString(hotkey.controller_keyseq);
|
||||
|
||||
if (overrides.count({group_name, action_name})) {
|
||||
const auto& overridden = overrides.at({group_name, action_name});
|
||||
portable_keyseq = QString::fromStdString(overridden.shortcut.keyseq);
|
||||
keyseq_str = QKeySequence(portable_keyseq).toString(QKeySequence::NativeText);
|
||||
controller_keyseq_str =
|
||||
QString::fromStdString(overridden.shortcut.controller_keyseq);
|
||||
}
|
||||
|
||||
auto* action_item = new QStandardItem(QCoreApplication::translate(
|
||||
"Hotkeys", qPrintable(QString::fromStdString(action_name))));
|
||||
action_item->setEditable(false);
|
||||
action_item->setData(QString::fromStdString(action_name), Qt::UserRole);
|
||||
|
||||
auto* keyseq_item = new QStandardItem(keyseq_str);
|
||||
keyseq_item->setData(portable_keyseq, Qt::UserRole);
|
||||
keyseq_item->setEditable(false);
|
||||
|
||||
auto* controller_item = new QStandardItem(controller_keyseq_str);
|
||||
controller_item->setEditable(false);
|
||||
|
||||
// Store metadata (context and repeat) for saving later
|
||||
int context = hotkey.context;
|
||||
bool repeat = hotkey.repeat;
|
||||
if (overrides.count({group_name, action_name})) {
|
||||
const auto& overridden = overrides.at({group_name, action_name});
|
||||
context = overridden.shortcut.context;
|
||||
repeat = overridden.shortcut.repeat;
|
||||
}
|
||||
action_item->setData(context, Qt::UserRole + 1);
|
||||
action_item->setData(repeat, Qt::UserRole + 2);
|
||||
|
||||
parent_item->appendRow({action_item, keyseq_item, controller_item});
|
||||
}
|
||||
|
||||
if (group_name == "General" || group_name == "Main Window") {
|
||||
ui->hotkey_list->expand(parent_item->index());
|
||||
}
|
||||
}
|
||||
ui->hotkey_list->expandAll();
|
||||
ui->hotkey_list->resizeColumnToContents(hotkey_column);
|
||||
ui->hotkey_list->resizeColumnToContents(controller_column);
|
||||
|
||||
// Re-apply column sizing after model reset
|
||||
ui->hotkey_list->header()->setStretchLastSection(false);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive);
|
||||
ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch);
|
||||
ui->hotkey_list->header()->setMinimumSectionSize(70);
|
||||
|
||||
ui->hotkey_list->setColumnWidth(name_column, 432);
|
||||
ui->hotkey_list->setColumnWidth(hotkey_column, 240);
|
||||
|
||||
// Enforce fixed width for Restore Defaults button to prevent smudging
|
||||
ui->button_restore_defaults->setFixedWidth(143);
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::UpdateProfileList() {
|
||||
const QSignalBlocker blocker(ui->combo_box_profile);
|
||||
ui->combo_box_profile->clear();
|
||||
|
||||
const auto& profiles = profile_manager.GetProfiles();
|
||||
for (const auto& [name, val] : profiles.profiles) {
|
||||
ui->combo_box_profile->addItem(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
ui->combo_box_profile->setCurrentText(QString::fromStdString(profiles.current_profile));
|
||||
Populate();
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnCreateProfile() {
|
||||
bool ok;
|
||||
QString text = QInputDialog::getText(this, tr("Create Profile"), tr("Profile Name:"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (ok && !text.isEmpty()) {
|
||||
if (profile_manager.CreateProfile(text.toStdString())) {
|
||||
// New profile is empty. Fill with current defaults or copy current?
|
||||
// "Defaults" logic usually implies defaults.
|
||||
UpdateProfileList();
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to create profile."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnDeleteProfile() {
|
||||
if (QMessageBox::question(this, tr("Delete Profile"),
|
||||
tr("Are you sure you want to delete this profile?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
if (profile_manager.DeleteProfile(ui->combo_box_profile->currentText().toStdString())) {
|
||||
UpdateProfileList();
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to delete profile."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnRenameProfile() {
|
||||
bool ok;
|
||||
QString current_name = ui->combo_box_profile->currentText();
|
||||
QString text = QInputDialog::getText(this, tr("Rename Profile"), tr("New Name:"),
|
||||
QLineEdit::Normal, current_name, &ok);
|
||||
if (ok && !text.isEmpty()) {
|
||||
if (profile_manager.RenameProfile(current_name.toStdString(), text.toStdString())) {
|
||||
UpdateProfileList();
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to rename profile."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnImportProfile() {
|
||||
QString fileName = QFileDialog::getOpenFileName(this, tr("Import Profile"), QString(),
|
||||
tr("JSON Files (*.json)"));
|
||||
if (fileName.isEmpty())
|
||||
return;
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to open file for reading."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray jsonData = file.readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonData);
|
||||
if (!doc.isObject()) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid profile format."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
if (!root.contains(QStringLiteral("shortcuts"))) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid profile file (missing shortcuts)."));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<Hotkey::BackendShortcut> shortcuts;
|
||||
const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray();
|
||||
for (const auto& val : arr) {
|
||||
shortcuts.push_back(Hotkey::ProfileManager::DeserializeShortcut(val.toObject()));
|
||||
}
|
||||
|
||||
ApplyShortcutsToModel(shortcuts);
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnExportProfile() {
|
||||
QString current = ui->combo_box_profile->currentText();
|
||||
QString fileName = QFileDialog::getSaveFileName(
|
||||
this, tr("Export Profile"), current + QStringLiteral(".json"), tr("JSON Files (*.json)"));
|
||||
if (fileName.isEmpty())
|
||||
return;
|
||||
|
||||
const std::vector<Hotkey::BackendShortcut> shortcuts = GatherShortcutsFromUI();
|
||||
|
||||
QJsonObject root_obj;
|
||||
root_obj[QStringLiteral("name")] = current;
|
||||
QJsonArray shortcuts_arr;
|
||||
for (const auto& s : shortcuts) {
|
||||
shortcuts_arr.append(Hotkey::ProfileManager::SerializeShortcut(s));
|
||||
}
|
||||
root_obj[QStringLiteral("shortcuts")] = shortcuts_arr;
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to open file for writing."));
|
||||
return;
|
||||
}
|
||||
|
||||
file.write(QJsonDocument(root_obj).toJson());
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnProfileChanged(int index) {
|
||||
if (index == -1)
|
||||
return;
|
||||
const std::string name = ui->combo_box_profile->currentText().toStdString();
|
||||
// Decoupled from permanent SetCurrentProfile to ensure "stagnant" behavior.
|
||||
Populate(name);
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::changeEvent(QEvent* event) {
|
||||
@@ -108,6 +324,7 @@ void ConfigureHotkeys::changeEvent(QEvent* event) {
|
||||
|
||||
void ConfigureHotkeys::RetranslateUI() {
|
||||
ui->retranslateUi(this);
|
||||
ui->label_profile->setText(tr("Hotkey Profile:"));
|
||||
|
||||
model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")});
|
||||
for (int key_id = 0; key_id < model->rowCount(); key_id++) {
|
||||
@@ -153,6 +370,7 @@ void ConfigureHotkeys::Configure(QModelIndex index) {
|
||||
tr("The entered key sequence is already assigned to: %1").arg(used_action));
|
||||
} else {
|
||||
model->setData(index, key_sequence.toString(QKeySequence::NativeText));
|
||||
model->setData(index, key_sequence.toString(QKeySequence::PortableText), Qt::UserRole);
|
||||
}
|
||||
}
|
||||
void ConfigureHotkeys::ConfigureController(QModelIndex index) {
|
||||
@@ -307,54 +525,61 @@ std::pair<bool, QString> ConfigureHotkeys::IsUsedControllerKey(const QString& ke
|
||||
return std::make_pair(false, QString());
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::ApplyConfiguration(HotkeyRegistry& registry) {
|
||||
for (int key_id = 0; key_id < model->rowCount(); key_id++) {
|
||||
const QStandardItem* parent = model->item(key_id, 0);
|
||||
for (int key_column_id = 0; key_column_id < parent->rowCount(); key_column_id++) {
|
||||
const QStandardItem* action = parent->child(key_column_id, name_column);
|
||||
const QStandardItem* keyseq = parent->child(key_column_id, hotkey_column);
|
||||
const QStandardItem* controller_keyseq =
|
||||
parent->child(key_column_id, controller_column);
|
||||
for (auto& [group, sub_actions] : registry.hotkey_groups) {
|
||||
if (group != parent->data().toString().toStdString())
|
||||
continue;
|
||||
for (auto& [action_name, hotkey] : sub_actions) {
|
||||
if (action_name != action->data().toString().toStdString())
|
||||
continue;
|
||||
hotkey.keyseq = QKeySequence(keyseq->text());
|
||||
hotkey.controller_keyseq = controller_keyseq->text().toStdString();
|
||||
}
|
||||
}
|
||||
void ConfigureHotkeys::ApplyConfiguration() {
|
||||
// 1. Sync the current profile selection permanently
|
||||
const std::string current_profile_name = ui->combo_box_profile->currentText().toStdString();
|
||||
profile_manager.SetCurrentProfile(current_profile_name);
|
||||
|
||||
// 2. Update the runtime HotkeyRegistry and UISettings
|
||||
const auto shortcuts = GatherShortcutsFromUI();
|
||||
|
||||
for (const auto& s : shortcuts) {
|
||||
// Update Registry
|
||||
auto& hk = registry.hotkey_groups[s.group][s.name];
|
||||
hk.keyseq = QKeySequence::fromString(QString::fromStdString(s.shortcut.keyseq));
|
||||
hk.controller_keyseq = s.shortcut.controller_keyseq;
|
||||
hk.context = static_cast<Qt::ShortcutContext>(s.shortcut.context);
|
||||
hk.repeat = s.shortcut.repeat;
|
||||
|
||||
if (hk.shortcut) {
|
||||
hk.shortcut->setKey(hk.keyseq);
|
||||
}
|
||||
if (hk.controller_shortcut) {
|
||||
hk.controller_shortcut->SetKey(hk.controller_keyseq);
|
||||
}
|
||||
}
|
||||
|
||||
// This will correctly populate UISettings::values.shortcuts based on current registry state
|
||||
registry.SaveHotkeys();
|
||||
|
||||
// 3. Update the ProfileManager (Storage)
|
||||
profile_manager.SetProfileShortcuts(current_profile_name, shortcuts);
|
||||
profile_manager.Save();
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::RestoreDefaults() {
|
||||
size_t hotkey_index = 0;
|
||||
const size_t total_default_hotkeys = UISettings::default_hotkeys.size();
|
||||
|
||||
for (int group_row = 0; group_row < model->rowCount(); ++group_row) {
|
||||
QStandardItem* parent = model->item(group_row, 0);
|
||||
const std::string group_name = parent->data(Qt::UserRole).toString().toStdString();
|
||||
|
||||
for (int child_row = 0; child_row < parent->rowCount(); ++child_row) {
|
||||
// This bounds check prevents a crash, and this was originally a safety check w/ showed if it failed,
|
||||
// however with further testing w/ restoring default functionality, it would work yet still display, so was changed to a regular Success!.
|
||||
if (hotkey_index >= total_default_hotkeys) {
|
||||
QMessageBox::information(this, tr("Success!"),
|
||||
tr("Citron's Default hotkey entries have been restored!"));
|
||||
return;
|
||||
QStandardItem* action_item = parent->child(child_row, name_column);
|
||||
const std::string action_name =
|
||||
action_item->data(Qt::UserRole).toString().toStdString();
|
||||
|
||||
// Find default
|
||||
for (const auto& def : UISettings::default_hotkeys) {
|
||||
if (def.group == group_name && def.name == action_name) {
|
||||
QStandardItem* hotkey_item = parent->child(child_row, hotkey_column);
|
||||
hotkey_item->setText(
|
||||
QKeySequence::fromString(QString::fromStdString(def.shortcut.keyseq))
|
||||
.toString(QKeySequence::NativeText));
|
||||
hotkey_item->setData(QString::fromStdString(def.shortcut.keyseq), Qt::UserRole);
|
||||
parent->child(child_row, controller_column)
|
||||
->setText(QString::fromStdString(def.shortcut.controller_keyseq));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto& default_shortcut = UISettings::default_hotkeys[hotkey_index].shortcut;
|
||||
|
||||
parent->child(child_row, hotkey_column)
|
||||
->setText(QString::fromStdString(default_shortcut.keyseq));
|
||||
parent->child(child_row, controller_column)
|
||||
->setText(QString::fromStdString(default_shortcut.controller_keyseq));
|
||||
|
||||
hotkey_index++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +591,9 @@ void ConfigureHotkeys::ClearAll() {
|
||||
const QStandardItem* parent = model->item(r, 0);
|
||||
|
||||
for (int r2 = 0; r2 < parent->rowCount(); ++r2) {
|
||||
model->item(r, 0)->child(r2, hotkey_column)->setText(QString{});
|
||||
QStandardItem* hotkey_item = model->item(r, 0)->child(r2, hotkey_column);
|
||||
hotkey_item->setText(QString{});
|
||||
hotkey_item->setData(QString{}, Qt::UserRole);
|
||||
model->item(r, 0)->child(r2, controller_column)->setText(QString{});
|
||||
}
|
||||
}
|
||||
@@ -401,8 +628,19 @@ void ConfigureHotkeys::PopupContextMenu(const QPoint& menu_location) {
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) {
|
||||
const QString& default_key_sequence =
|
||||
QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.controller_keyseq);
|
||||
const auto* group_item = model->itemFromIndex(index.parent());
|
||||
const auto* action_item = group_item->child(index.row(), name_column);
|
||||
const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
|
||||
const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString();
|
||||
|
||||
QString default_key_sequence;
|
||||
for (const auto& def : UISettings::default_hotkeys) {
|
||||
if (def.group == group_name && def.name == action_name) {
|
||||
default_key_sequence = QString::fromStdString(def.shortcut.controller_keyseq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto [key_sequence_used, used_action] = IsUsedControllerKey(default_key_sequence);
|
||||
|
||||
if (key_sequence_used && default_key_sequence != model->data(index).toString()) {
|
||||
@@ -415,16 +653,85 @@ void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) {
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::RestoreHotkey(QModelIndex index) {
|
||||
const QKeySequence& default_key_sequence = QKeySequence::fromString(
|
||||
QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.keyseq),
|
||||
QKeySequence::NativeText);
|
||||
const auto* group_item = model->itemFromIndex(index.parent());
|
||||
const auto* action_item = group_item->child(index.row(), name_column);
|
||||
const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
|
||||
const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString();
|
||||
|
||||
QString default_key_str;
|
||||
for (const auto& def : UISettings::default_hotkeys) {
|
||||
if (def.group == group_name && def.name == action_name) {
|
||||
default_key_str = QString::fromStdString(def.shortcut.keyseq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const QKeySequence& default_key_sequence =
|
||||
QKeySequence::fromString(default_key_str, QKeySequence::NativeText);
|
||||
const auto [key_sequence_used, used_action] = IsUsedKey(default_key_sequence);
|
||||
|
||||
if (key_sequence_used && default_key_sequence != QKeySequence(model->data(index).toString())) {
|
||||
QMessageBox::warning(
|
||||
this, tr("Conflicting Key Sequence"),
|
||||
tr("The default key sequence is already assigned to: %1").arg(used_action));
|
||||
} else {
|
||||
model->setData(index, default_key_sequence.toString(QKeySequence::NativeText));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Hotkey::BackendShortcut> ConfigureHotkeys::GatherShortcutsFromUI() const {
|
||||
std::vector<Hotkey::BackendShortcut> shortcuts;
|
||||
const auto& root = model->invisibleRootItem();
|
||||
for (int group_row = 0; group_row < root->rowCount(); group_row++) {
|
||||
const auto* group_item = root->child(group_row);
|
||||
const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
|
||||
for (int row = 0; row < group_item->rowCount(); row++) {
|
||||
const auto* action_item = group_item->child(row, name_column);
|
||||
const auto* keyseq_item = group_item->child(row, hotkey_column);
|
||||
const auto* controller_item = group_item->child(row, controller_column);
|
||||
|
||||
Hotkey::BackendShortcut s;
|
||||
s.group = group_name;
|
||||
s.name = action_item->data(Qt::UserRole).toString().toStdString();
|
||||
s.shortcut.keyseq = keyseq_item->data(Qt::UserRole).toString().toStdString();
|
||||
s.shortcut.controller_keyseq = controller_item->text().toStdString();
|
||||
s.shortcut.context = action_item->data(Qt::UserRole + 1).toInt();
|
||||
s.shortcut.repeat = action_item->data(Qt::UserRole + 2).toBool();
|
||||
shortcuts.push_back(s);
|
||||
}
|
||||
}
|
||||
return shortcuts;
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::ApplyShortcutsToModel(
|
||||
const std::vector<Hotkey::BackendShortcut>& shortcuts) {
|
||||
// Map for faster lookup
|
||||
std::map<std::pair<std::string, std::string>, Hotkey::BackendShortcut> shortcut_map;
|
||||
for (const auto& s : shortcuts) {
|
||||
shortcut_map[{s.group, s.name}] = s;
|
||||
}
|
||||
|
||||
const auto& root = model->invisibleRootItem();
|
||||
for (int group_row = 0; group_row < root->rowCount(); group_row++) {
|
||||
auto* group_item = root->child(group_row);
|
||||
const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
|
||||
for (int row = 0; row < group_item->rowCount(); row++) {
|
||||
auto* action_item = group_item->child(row, name_column);
|
||||
const std::string action_name =
|
||||
action_item->data(Qt::UserRole).toString().toStdString();
|
||||
|
||||
if (shortcut_map.count({group_name, action_name})) {
|
||||
const auto& s = shortcut_map.at({group_name, action_name});
|
||||
QStandardItem* hotkey_item = group_item->child(row, hotkey_column);
|
||||
hotkey_item->setText(QKeySequence(QString::fromStdString(s.shortcut.keyseq))
|
||||
.toString(QKeySequence::NativeText));
|
||||
hotkey_item->setData(QString::fromStdString(s.shortcut.keyseq), Qt::UserRole);
|
||||
|
||||
model->setData(model->index(row, controller_column, group_item->index()),
|
||||
QString::fromStdString(s.shortcut.controller_keyseq));
|
||||
model->setData(model->index(row, name_column, group_item->index()),
|
||||
s.shortcut.context, Qt::UserRole + 1);
|
||||
model->setData(model->index(row, name_column, group_item->index()),
|
||||
s.shortcut.repeat, Qt::UserRole + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QStandardItemModel>
|
||||
#include <QWidget>
|
||||
#include "citron/hotkey_profile_manager.h"
|
||||
|
||||
namespace Common {
|
||||
class ParamPackage;
|
||||
@@ -28,21 +24,31 @@ class ConfigureHotkeys : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfigureHotkeys(Core::HID::HIDCore& hid_core_, QWidget* parent = nullptr);
|
||||
explicit ConfigureHotkeys(HotkeyRegistry& registry, Core::HID::HIDCore& hid_core_,
|
||||
QWidget* parent = nullptr);
|
||||
~ConfigureHotkeys() override;
|
||||
|
||||
void ApplyConfiguration(HotkeyRegistry& registry);
|
||||
void ApplyConfiguration();
|
||||
|
||||
/**
|
||||
* Populates the hotkey list widget using data from the provided registry.
|
||||
* Populates the hotkey list widget using data from the provided profiles.
|
||||
* Called every time the Configure dialog is opened.
|
||||
* @param registry The HotkeyRegistry whose data is used to populate the list.
|
||||
* @param profiles The UserHotkeyProfiles used to populate the list.
|
||||
*/
|
||||
void Populate(const HotkeyRegistry& registry);
|
||||
void Populate(const std::string& profile_name = "");
|
||||
|
||||
private slots:
|
||||
void OnCreateProfile();
|
||||
void OnDeleteProfile();
|
||||
void OnRenameProfile();
|
||||
void OnImportProfile();
|
||||
void OnExportProfile();
|
||||
void OnProfileChanged(int index);
|
||||
|
||||
private:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void RetranslateUI();
|
||||
void UpdateProfileList();
|
||||
|
||||
void Configure(QModelIndex index);
|
||||
void ConfigureController(QModelIndex index);
|
||||
@@ -55,10 +61,15 @@ private:
|
||||
void RestoreControllerHotkey(QModelIndex index);
|
||||
void RestoreHotkey(QModelIndex index);
|
||||
|
||||
std::vector<Hotkey::BackendShortcut> GatherShortcutsFromUI() const;
|
||||
void ApplyShortcutsToModel(const std::vector<Hotkey::BackendShortcut>& shortcuts);
|
||||
|
||||
void SetPollingResult(bool cancel);
|
||||
QString GetButtonCombinationName(Core::HID::NpadButton button, bool home, bool capture) const;
|
||||
|
||||
std::unique_ptr<Ui::ConfigureHotkeys> ui;
|
||||
Hotkey::ProfileManager profile_manager;
|
||||
HotkeyRegistry& registry;
|
||||
|
||||
QStandardItemModel* model;
|
||||
|
||||
|
||||
@@ -18,16 +18,65 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_ProfileSelect">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<widget class="QLabel" name="label_profile">
|
||||
<property name="text">
|
||||
<string>Double-click on a binding to change it.</string>
|
||||
<string>Hotkey Profile:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<widget class="QComboBox" name="combo_box_profile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_Actions">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_new_profile">
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_delete_profile">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_rename_profile">
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_import_profile">
|
||||
<property name="text">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_export_profile">
|
||||
<property name="text">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_Buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
@@ -48,6 +97,24 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_restore_defaults">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>139</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>139</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Restore Defaults</string>
|
||||
</property>
|
||||
@@ -55,6 +122,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Double-click on a binding to change it.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
|
||||
@@ -8,14 +8,21 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStandardItemModel>
|
||||
#include <QString>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
|
||||
#include "citron/configuration/configure_per_game_cheats.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/string_util.h"
|
||||
@@ -28,7 +35,6 @@
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/memory/cheat_engine.h"
|
||||
#include "ui_configure_per_game_cheats.h"
|
||||
#include "citron/configuration/configure_per_game_cheats.h"
|
||||
|
||||
ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* parent)
|
||||
: QWidget(parent), ui{std::make_unique<Ui::ConfigurePerGameCheats>()}, system{system_} {
|
||||
@@ -46,7 +52,10 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
|
||||
tree_view->setSortingEnabled(true);
|
||||
tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
|
||||
tree_view->setUniformRowHeights(true);
|
||||
tree_view->setContextMenuPolicy(Qt::NoContextMenu);
|
||||
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
connect(tree_view, &QTreeView::customContextMenuRequested, this,
|
||||
&ConfigurePerGameCheats::OnContextMenu);
|
||||
|
||||
item_model->insertColumns(0, 1);
|
||||
item_model->setHeaderData(0, Qt::Horizontal, tr("Cheat Name"));
|
||||
@@ -64,9 +73,11 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
|
||||
enable_all_button = new QPushButton(tr("Enable All"));
|
||||
disable_all_button = new QPushButton(tr("Disable All"));
|
||||
save_button = new QPushButton(tr("Save"));
|
||||
refresh_button = new QPushButton(tr("Refresh"));
|
||||
|
||||
button_layout->addWidget(enable_all_button);
|
||||
button_layout->addWidget(disable_all_button);
|
||||
button_layout->addWidget(refresh_button);
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(save_button);
|
||||
|
||||
@@ -83,8 +94,8 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
|
||||
&ConfigurePerGameCheats::EnableAllCheats);
|
||||
connect(disable_all_button, &QPushButton::clicked, this,
|
||||
&ConfigurePerGameCheats::DisableAllCheats);
|
||||
connect(save_button, &QPushButton::clicked, this,
|
||||
&ConfigurePerGameCheats::SaveCheatSettings);
|
||||
connect(save_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::SaveCheatSettings);
|
||||
connect(refresh_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::RefreshCheats);
|
||||
}
|
||||
|
||||
ConfigurePerGameCheats::~ConfigurePerGameCheats() = default;
|
||||
@@ -194,7 +205,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
FileSys::XCI xci(file, title_id, 0);
|
||||
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
||||
auto program_nca = xci.GetNCAByType(FileSys::NCAContentType::Program);
|
||||
if (program_nca && program_nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||
if (program_nca &&
|
||||
program_nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||
auto exefs = program_nca->GetExeFS();
|
||||
if (exefs) {
|
||||
main_nso = exefs->GetFile("main");
|
||||
@@ -241,7 +253,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
if (load_dir) {
|
||||
auto patch_dirs = load_dir->GetSubdirectories();
|
||||
for (const auto& subdir : patch_dirs) {
|
||||
if (!subdir) continue;
|
||||
if (!subdir)
|
||||
continue;
|
||||
|
||||
// Use case-insensitive directory search (same as FindSubdirectoryCaseless)
|
||||
FileSys::VirtualDir cheats_dir;
|
||||
@@ -288,8 +301,10 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
try {
|
||||
// Pad to full 64 chars (32 bytes) with zeros
|
||||
// Keep the case as-is from the filename
|
||||
auto full_build_id_hex = potential_build_id + std::string(48, '0');
|
||||
auto build_id_bytes = Common::HexStringToArray<0x20>(full_build_id_hex);
|
||||
auto full_build_id_hex =
|
||||
potential_build_id + std::string(48, '0');
|
||||
auto build_id_bytes =
|
||||
Common::HexStringToArray<0x20>(full_build_id_hex);
|
||||
|
||||
// Verify the result is not all zeros
|
||||
bool is_valid_result = false;
|
||||
@@ -311,7 +326,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_build_id) break;
|
||||
if (has_build_id)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,9 +369,9 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
// Add cheats to tree view
|
||||
for (const auto& cheat : cheats) {
|
||||
// Extract cheat name from readable_name (null-terminated)
|
||||
const std::string cheat_name_str(cheat.definition.readable_name.data(),
|
||||
strnlen(cheat.definition.readable_name.data(),
|
||||
cheat.definition.readable_name.size()));
|
||||
const std::string cheat_name_str(
|
||||
cheat.definition.readable_name.data(),
|
||||
strnlen(cheat.definition.readable_name.data(), cheat.definition.readable_name.size()));
|
||||
|
||||
// Skip empty cheat names or cheats with no opcodes
|
||||
if (cheat_name_str.empty() || cheat.definition.num_opcodes == 0) {
|
||||
@@ -369,7 +385,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
|
||||
cheat_item->setCheckable(true);
|
||||
|
||||
// Check if cheat is disabled
|
||||
const bool cheat_disabled = disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end();
|
||||
const bool cheat_disabled =
|
||||
disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end();
|
||||
cheat_item->setCheckState(cheat_disabled ? Qt::Unchecked : Qt::Checked);
|
||||
|
||||
list_items.push_back(QList<QStandardItem*>{cheat_item});
|
||||
@@ -474,3 +491,26 @@ void ConfigurePerGameCheats::ReloadCheatEngine() const {
|
||||
const auto cheats = pm.CreateCheatList(current_build_id);
|
||||
cheat_engine->Reload(cheats);
|
||||
}
|
||||
|
||||
void ConfigurePerGameCheats::OnContextMenu(const QPoint& pos) {
|
||||
const auto index = tree_view->indexAt(pos);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMenu context_menu;
|
||||
|
||||
auto* open_folder_action = context_menu.addAction(tr("Open Cheats Folder"));
|
||||
connect(open_folder_action, &QAction::triggered, this, [this] {
|
||||
const auto cheats_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir) /
|
||||
fmt::format("{:016X}", title_id) / "cheats";
|
||||
QDir().mkpath(QString::fromStdString(cheats_dir.string()));
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(cheats_dir.string())));
|
||||
});
|
||||
|
||||
context_menu.exec(tree_view->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void ConfigurePerGameCheats::RefreshCheats() {
|
||||
LoadConfiguration();
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ private:
|
||||
void SaveCheatSettings();
|
||||
void SetAllCheats(bool enabled);
|
||||
void ReloadCheatEngine() const;
|
||||
void OnContextMenu(const QPoint& pos);
|
||||
void RefreshCheats();
|
||||
|
||||
std::unique_ptr<Ui::ConfigurePerGameCheats> ui;
|
||||
FileSys::VirtualFile file;
|
||||
@@ -61,6 +63,7 @@ private:
|
||||
QPushButton* enable_all_button;
|
||||
QPushButton* disable_all_button;
|
||||
QPushButton* save_button;
|
||||
QPushButton* refresh_button;
|
||||
|
||||
QVBoxLayout* layout;
|
||||
QTreeView* tree_view;
|
||||
|
||||
@@ -44,7 +44,7 @@ protected:
|
||||
void SaveUIValues() override;
|
||||
void SaveUIGamelistValues() override;
|
||||
void SaveUILayoutValues() override;
|
||||
void SaveMultiplayerValues();
|
||||
void SaveMultiplayerValues() override;
|
||||
void SaveNetworkValues();
|
||||
|
||||
public:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,27 +12,29 @@
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QListView>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QSlider>
|
||||
#include <QStandardItemModel>
|
||||
#include <QString>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
|
||||
#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);
|
||||
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "citron/play_time_manager.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "citron/util/util.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
|
||||
enum class GameListItemType {
|
||||
Game = QStandardItem::UserType + 1,
|
||||
@@ -62,7 +63,8 @@ static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) {
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Create a rounded rectangle clipping path
|
||||
const int radius = size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding)
|
||||
const int radius =
|
||||
size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding)
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(0, 0, size, size, radius, radius);
|
||||
painter.setClipPath(path);
|
||||
@@ -98,6 +100,7 @@ public:
|
||||
static constexpr int FullPathRole = SortRole + 2;
|
||||
static constexpr int ProgramIdRole = SortRole + 3;
|
||||
static constexpr int FileTypeRole = SortRole + 4;
|
||||
static constexpr int HighResIconRole = SortRole + 5;
|
||||
|
||||
GameListItemPath() = default;
|
||||
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
|
||||
@@ -115,6 +118,9 @@ public:
|
||||
picture = GetDefaultIcon(size);
|
||||
}
|
||||
|
||||
// Store unscaled pixmap for high-quality animations
|
||||
setData(picture, HighResIconRole);
|
||||
|
||||
// Create a round icon
|
||||
QPixmap round_picture = CreateRoundIcon(picture, size);
|
||||
|
||||
|
||||
@@ -11,16 +11,22 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
|
||||
#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<Loader::FileType>(entry[QStringLiteral("file_type")].toInt());
|
||||
metadata.file_size = static_cast<std::size_t>(entry[QStringLiteral("file_size")].toVariant().toULongLong());
|
||||
metadata.program_id =
|
||||
entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16);
|
||||
metadata.file_type =
|
||||
static_cast<Loader::FileType>(entry[QStringLiteral("file_type")].toInt());
|
||||
metadata.file_size =
|
||||
static_cast<std::size_t>(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<qint64>(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<qint64>(metadata.modification_time);
|
||||
entry[QStringLiteral("modification_time")] =
|
||||
static_cast<qint64>(metadata.modification_time);
|
||||
|
||||
const QByteArray icon_data(reinterpret_cast<const char*>(metadata.icon.data()),
|
||||
static_cast<int>(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<std::chrono::seconds>(
|
||||
mod_time.time_since_epoch()).count();
|
||||
const auto mod_time_seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(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<u8>& icon) {
|
||||
std::size_t file_size, const std::string& title,
|
||||
const std::vector<u8>& 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<std::chrono::seconds>(
|
||||
mod_time.time_since_epoch()).count();
|
||||
const auto mod_time_seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(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<QStandardItem*> MakeGameListEntry(
|
||||
const std::string& path, const std::string& name, const std::size_t size,
|
||||
const std::vector<u8>& icon, Loader::AppLoader& loader, u64 program_id,
|
||||
const CompatibilityList& compatibility_list, const PlayTime::PlayTimeManager& play_time_manager,
|
||||
const FileSys::PatchManager& patch,
|
||||
const std::map<u64, std::pair<int, int>>& online_stats) {
|
||||
QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
|
||||
const std::size_t size, const std::vector<u8>& icon,
|
||||
Loader::AppLoader& loader, u64 program_id,
|
||||
const CompatibilityList& compatibility_list,
|
||||
const PlayTime::PlayTimeManager& play_time_manager,
|
||||
const FileSys::PatchManager& patch,
|
||||
const std::map<u64, std::pair<int, int>>& 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<QStandardItem*> 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<QStandardItem*> 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<QStandardItem*> 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<u64, std::pair<int, int>>& online_stats) {
|
||||
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir,
|
||||
const std::map<u64, std::pair<int, int>>& 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<u64, std::pair<int, int>>& online_stats,
|
||||
GameListDir* parent_dir,
|
||||
const std::map<u64, std::pair<int, int>>& 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<FileSys::NSP>(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<FileSys::NSP>(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<FileSys::NSP>(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); });
|
||||
|
||||
250
src/citron/hotkey_profile_manager.cpp
Normal file
250
src/citron/hotkey_profile_manager.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
// SPDX-FileCopyrightText: 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "citron/hotkey_profile_manager.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Hotkey {
|
||||
|
||||
ProfileManager::ProfileManager() {
|
||||
Load();
|
||||
}
|
||||
|
||||
ProfileManager::~ProfileManager() = default;
|
||||
|
||||
static std::string GetSaveFilePath() {
|
||||
const auto save_dir =
|
||||
Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir); // Saved in ConfigDir now
|
||||
return Common::FS::PathToUTF8String(save_dir / "hotkey_profiles.json");
|
||||
}
|
||||
|
||||
// JSON Serialization Helpers
|
||||
QJsonObject ProfileManager::SerializeShortcut(const BackendShortcut& shortcut) {
|
||||
QJsonObject obj;
|
||||
obj[QStringLiteral("name")] = QString::fromStdString(shortcut.name);
|
||||
obj[QStringLiteral("group")] = QString::fromStdString(shortcut.group);
|
||||
obj[QStringLiteral("keyseq")] = QString::fromStdString(shortcut.shortcut.keyseq);
|
||||
obj[QStringLiteral("controller_keyseq")] =
|
||||
QString::fromStdString(shortcut.shortcut.controller_keyseq);
|
||||
obj[QStringLiteral("context")] = shortcut.shortcut.context;
|
||||
obj[QStringLiteral("repeat")] = shortcut.shortcut.repeat;
|
||||
return obj;
|
||||
}
|
||||
|
||||
BackendShortcut ProfileManager::DeserializeShortcut(const QJsonObject& obj) {
|
||||
BackendShortcut s;
|
||||
s.name = obj[QStringLiteral("name")].toString().toStdString();
|
||||
s.group = obj[QStringLiteral("group")].toString().toStdString();
|
||||
s.shortcut.keyseq = obj[QStringLiteral("keyseq")].toString().toStdString();
|
||||
s.shortcut.controller_keyseq =
|
||||
obj[QStringLiteral("controller_keyseq")].toString().toStdString();
|
||||
s.shortcut.context = obj[QStringLiteral("context")].toInt();
|
||||
s.shortcut.repeat = obj[QStringLiteral("repeat")].toBool();
|
||||
return s;
|
||||
}
|
||||
|
||||
void ProfileManager::Load() {
|
||||
const auto path = GetSaveFilePath();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_INFO(Config, "hotkey_profiles.json not found, creating new.");
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonObject root = doc.object();
|
||||
|
||||
profiles.profiles.clear();
|
||||
|
||||
if (root.contains(QStringLiteral("current_profile"))) {
|
||||
profiles.current_profile = root[QStringLiteral("current_profile")].toString().toStdString();
|
||||
}
|
||||
|
||||
if (root.contains(QStringLiteral("profiles"))) {
|
||||
const QJsonObject profiles_obj = root[QStringLiteral("profiles")].toObject();
|
||||
for (auto it = profiles_obj.begin(); it != profiles_obj.end(); ++it) {
|
||||
const QString profile_name = it.key();
|
||||
const QJsonArray shortcuts_arr = it.value().toArray();
|
||||
std::vector<BackendShortcut> shortcuts;
|
||||
for (const auto& val : shortcuts_arr) {
|
||||
shortcuts.push_back(DeserializeShortcut(val.toObject()));
|
||||
}
|
||||
profiles.profiles[profile_name.toStdString()] = shortcuts;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure default profile exists
|
||||
if (profiles.profiles.empty()) {
|
||||
profiles.profiles["Default"] = {};
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileManager::Save() {
|
||||
const auto path = GetSaveFilePath();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_ERROR(Config, "Failed to open hotkey_profiles.json for writing.");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root[QStringLiteral("current_profile")] = QString::fromStdString(profiles.current_profile);
|
||||
|
||||
QJsonObject profiles_obj;
|
||||
for (const auto& [name, shortcuts] : profiles.profiles) {
|
||||
QJsonArray shortcuts_arr;
|
||||
for (const auto& s : shortcuts) {
|
||||
shortcuts_arr.append(SerializeShortcut(s));
|
||||
}
|
||||
profiles_obj[QString::fromStdString(name)] = shortcuts_arr;
|
||||
}
|
||||
root[QStringLiteral("profiles")] = profiles_obj;
|
||||
|
||||
file.write(QJsonDocument(root).toJson());
|
||||
}
|
||||
|
||||
bool ProfileManager::CreateProfile(const std::string& profile_name) {
|
||||
if (profile_name.empty())
|
||||
return false;
|
||||
|
||||
if (profiles.profiles.size() >= MAX_PROFILES) {
|
||||
return false;
|
||||
}
|
||||
if (profiles.profiles.count(profile_name)) {
|
||||
return false; // Already exists
|
||||
}
|
||||
|
||||
profiles.profiles[profile_name] = {}; // Create empty, populated later by UI
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::DeleteProfile(const std::string& profile_name) {
|
||||
if (profile_name == "Default")
|
||||
return false; // Cannot delete default
|
||||
|
||||
if (profiles.profiles.erase(profile_name)) {
|
||||
if (profiles.current_profile == profile_name) {
|
||||
profiles.current_profile = "Default";
|
||||
}
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProfileManager::RenameProfile(const std::string& old_name, const std::string& new_name) {
|
||||
if (old_name == "Default")
|
||||
return false; // Cannot rename default
|
||||
if (new_name.empty())
|
||||
return false;
|
||||
|
||||
if (!profiles.profiles.count(old_name))
|
||||
return false;
|
||||
if (profiles.profiles.count(new_name))
|
||||
return false;
|
||||
|
||||
auto node = profiles.profiles.extract(old_name);
|
||||
node.key() = new_name;
|
||||
profiles.profiles.insert(std::move(node));
|
||||
|
||||
if (profiles.current_profile == old_name) {
|
||||
profiles.current_profile = new_name;
|
||||
}
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::SetCurrentProfile(const std::string& profile_name) {
|
||||
if (!profiles.profiles.count(profile_name))
|
||||
return false;
|
||||
|
||||
profiles.current_profile = profile_name;
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfileManager::SetProfileShortcuts(const std::string& profile_name,
|
||||
const std::vector<BackendShortcut>& shortcuts) {
|
||||
if (profiles.profiles.count(profile_name)) {
|
||||
profiles.profiles[profile_name] = shortcuts;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProfileManager::ExportProfile(const std::string& profile_name, const std::string& file_path) {
|
||||
if (!profiles.profiles.count(profile_name))
|
||||
return false;
|
||||
|
||||
QJsonObject root;
|
||||
root[QStringLiteral("name")] = QString::fromStdString(profile_name);
|
||||
|
||||
QJsonArray shortcuts_arr;
|
||||
for (const auto& s : profiles.profiles.at(profile_name)) {
|
||||
shortcuts_arr.append(SerializeShortcut(s));
|
||||
}
|
||||
root[QStringLiteral("shortcuts")] = shortcuts_arr;
|
||||
|
||||
QFile file(QString::fromStdString(file_path));
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
|
||||
file.write(QJsonDocument(root).toJson());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::ImportProfile(const std::string& file_path) {
|
||||
return !ImportProfileAndGetFinalName(file_path).empty();
|
||||
}
|
||||
|
||||
std::string ProfileManager::ImportProfileAndGetFinalName(const std::string& file_path) {
|
||||
QFile file(QString::fromStdString(file_path));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonObject root = doc.object();
|
||||
|
||||
if (!root.contains(QStringLiteral("name")) || !root.contains(QStringLiteral("shortcuts")))
|
||||
return {};
|
||||
|
||||
std::string profile_name = root[QStringLiteral("name")].toString().toStdString();
|
||||
if (profile_name.empty()) {
|
||||
profile_name = "Imported Profile";
|
||||
}
|
||||
|
||||
// Handle name collision
|
||||
std::string base_name = profile_name;
|
||||
int suffix = 1;
|
||||
while (profiles.profiles.count(profile_name)) {
|
||||
profile_name = base_name + " (" + std::to_string(suffix++) + ")";
|
||||
}
|
||||
|
||||
if (profiles.profiles.size() >= MAX_PROFILES)
|
||||
return {};
|
||||
|
||||
std::vector<BackendShortcut> shortcuts;
|
||||
const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray();
|
||||
for (const auto& val : arr) {
|
||||
shortcuts.push_back(DeserializeShortcut(val.toObject()));
|
||||
}
|
||||
|
||||
profiles.profiles[profile_name] = shortcuts;
|
||||
Save();
|
||||
return profile_name;
|
||||
}
|
||||
|
||||
} // namespace Hotkey
|
||||
74
src/citron/hotkey_profile_manager.h
Normal file
74
src/citron/hotkey_profile_manager.h
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "common/uuid.h"
|
||||
|
||||
namespace Hotkey {
|
||||
|
||||
// A backend-only representation of a shortcut, free of any Qt types.
|
||||
struct BackendContextualShortcut {
|
||||
std::string keyseq;
|
||||
std::string controller_keyseq;
|
||||
int context;
|
||||
bool repeat;
|
||||
};
|
||||
|
||||
struct BackendShortcut {
|
||||
std::string name;
|
||||
std::string group;
|
||||
BackendContextualShortcut shortcut;
|
||||
};
|
||||
|
||||
// Contains all hotkey profile data for a single user
|
||||
struct UserHotkeyProfiles {
|
||||
std::map<std::string, std::vector<BackendShortcut>> profiles;
|
||||
std::string current_profile = "Default";
|
||||
};
|
||||
|
||||
class ProfileManager {
|
||||
public:
|
||||
ProfileManager();
|
||||
~ProfileManager();
|
||||
|
||||
// Profile Access
|
||||
const UserHotkeyProfiles& GetProfiles() const {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
// Profile Management
|
||||
bool CreateProfile(const std::string& profile_name);
|
||||
bool DeleteProfile(const std::string& profile_name);
|
||||
bool RenameProfile(const std::string& old_name, const std::string& new_name);
|
||||
bool SetCurrentProfile(const std::string& profile_name);
|
||||
void SetProfileShortcuts(const std::string& profile_name,
|
||||
const std::vector<BackendShortcut>& shortcuts);
|
||||
|
||||
// Import/Export
|
||||
bool ExportProfile(const std::string& profile_name, const std::string& file_path);
|
||||
bool ImportProfile(const std::string& file_path);
|
||||
std::string ImportProfileAndGetFinalName(const std::string& file_path);
|
||||
|
||||
// JSON Serialization Helpers
|
||||
static QJsonObject SerializeShortcut(const BackendShortcut& shortcut);
|
||||
static BackendShortcut DeserializeShortcut(const QJsonObject& obj);
|
||||
|
||||
// IO
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
// Constants
|
||||
static constexpr size_t MAX_PROFILES = 5;
|
||||
|
||||
private:
|
||||
// Global profiles data
|
||||
UserHotkeyProfiles profiles;
|
||||
};
|
||||
|
||||
} // namespace Hotkey
|
||||
@@ -7,9 +7,10 @@
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "hid_core/frontend/emulated_controller.h"
|
||||
#include "citron/hotkeys.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "hid_core/frontend/emulated_controller.h"
|
||||
|
||||
|
||||
HotkeyRegistry::HotkeyRegistry() = default;
|
||||
HotkeyRegistry::~HotkeyRegistry() = default;
|
||||
@@ -46,10 +47,10 @@ void HotkeyRegistry::SaveHotkeys() {
|
||||
if (is_modified) {
|
||||
UISettings::values.shortcuts.push_back(
|
||||
{action_name, group_name,
|
||||
UISettings::ContextualShortcut(
|
||||
{current_hotkey.keyseq.toString().toStdString(),
|
||||
current_hotkey.controller_keyseq, current_hotkey.context,
|
||||
current_hotkey.repeat})});
|
||||
UISettings::ContextualShortcut({current_hotkey.keyseq.toString().toStdString(),
|
||||
current_hotkey.controller_keyseq,
|
||||
current_hotkey.context,
|
||||
current_hotkey.repeat})});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,8 +71,7 @@ void HotkeyRegistry::LoadHotkeys() {
|
||||
for (const auto& shortcut : UISettings::values.shortcuts) {
|
||||
Hotkey& hk = hotkey_groups[shortcut.group][shortcut.name];
|
||||
if (!shortcut.shortcut.keyseq.empty()) {
|
||||
hk.keyseq = QKeySequence::fromString(QString::fromStdString(shortcut.shortcut.keyseq),
|
||||
QKeySequence::NativeText);
|
||||
hk.keyseq = QKeySequence::fromString(QString::fromStdString(shortcut.shortcut.keyseq));
|
||||
} else {
|
||||
// This is the fix: explicitly clear the key sequence if it was saved as empty.
|
||||
hk.keyseq = QKeySequence();
|
||||
@@ -101,9 +101,9 @@ QShortcut* HotkeyRegistry::GetHotkey(const std::string& group, const std::string
|
||||
return hk.shortcut;
|
||||
}
|
||||
|
||||
ControllerShortcut* HotkeyRegistry::GetControllerHotkey(const std::string& group,
|
||||
const std::string& action,
|
||||
Core::HID::EmulatedController* controller) const {
|
||||
ControllerShortcut* HotkeyRegistry::GetControllerHotkey(
|
||||
const std::string& group, const std::string& action,
|
||||
Core::HID::EmulatedController* controller) const {
|
||||
Hotkey& hk = hotkey_groups[group][action];
|
||||
|
||||
if (!hk.controller_shortcut) {
|
||||
@@ -114,7 +114,8 @@ ControllerShortcut* HotkeyRegistry::GetControllerHotkey(const std::string& group
|
||||
return hk.controller_shortcut;
|
||||
}
|
||||
|
||||
QKeySequence HotkeyRegistry::GetKeySequence(const std::string& group, const std::string& action) const {
|
||||
QKeySequence HotkeyRegistry::GetKeySequence(const std::string& group,
|
||||
const std::string& action) const {
|
||||
return hotkey_groups[group][action].keyseq;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,23 +4,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <QMainWindow>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#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 <QVariant>
|
||||
@@ -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<QtConfig> 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<FileSys::VfsFilesystem>& 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<FileSys::VfsFilesystem>& 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<Common::UUID> 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<Service::NFC::NfcDevice> nfp_device);
|
||||
void AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters,
|
||||
std::shared_ptr<Service::NFC::NfcDevice> 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<size_t, InputCommon::TasInput::PLAYER_NUMBER> frames) const;
|
||||
#ifdef __unix__
|
||||
std::string CreateTASFramesString(
|
||||
std::array<size_t, InputCommon::TasInput::PLAYER_NUMBER> 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<FileSys::ManualContentProvider> 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<Core::System> system;
|
||||
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
||||
std::unique_ptr<Ui::MainWindow> 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<int, 3> sig_interrupt_fds;
|
||||
#endif
|
||||
#endif
|
||||
protected:
|
||||
void dropEvent(QDropEvent* event) override;
|
||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||
|
||||
@@ -192,6 +192,8 @@
|
||||
<addaction name="action_Report_Compatibility"/>
|
||||
<addaction name="action_Open_Support"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Open_Log_Folder"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Check_For_Updates"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_About"/>
|
||||
@@ -450,6 +452,11 @@
|
||||
<string>Open &citron Folder</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Open_Log_Folder">
|
||||
<property name="text">
|
||||
<string>Open &Log Folder</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Capture_Screenshot">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "citron/updater/updater_service.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "citron/updater/updater_service.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scm_rev.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QStandardPaths>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QRegularExpression>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTimer>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSslConfiguration>
|
||||
#include <QCoreApplication>
|
||||
#include <QSslSocket>
|
||||
#include <QCryptographicHash>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QSslConfiguration>
|
||||
#include <QSslSocket>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef CITRON_ENABLE_LIBARCHIVE
|
||||
#include <archive.h>
|
||||
@@ -41,11 +41,26 @@
|
||||
|
||||
namespace Updater {
|
||||
|
||||
const std::string STABLE_UPDATE_URL = "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases";
|
||||
const std::string NIGHTLY_UPDATE_URL = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
|
||||
const std::string STABLE_UPDATE_URL =
|
||||
"https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases";
|
||||
const std::string NIGHTLY_UPDATE_URL =
|
||||
"https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
|
||||
|
||||
std::string ExtractCommitHash(const std::string& version_string) {
|
||||
std::regex re("\\b([0-9a-fA-F]{7,40})\\b");
|
||||
// Hashes in git describe often start with 'g'.
|
||||
// We match 7-40 hex characters, optionally preceded by 'g'.
|
||||
std::regex re("(?:\\b|[gG])([0-9a-fA-F]{7,40})\\b");
|
||||
std::smatch match;
|
||||
if (std::regex_search(version_string, match, re) && match.size() > 1) {
|
||||
return match[1].str();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ExtractVersionTag(const std::string& version_string) {
|
||||
// Matches tag parts like v1.2.3 or 2026.02.1 at the start of the string.
|
||||
// We stop at the first hyphen to avoid the -count-gHASH part.
|
||||
std::regex re("^v?([0-9.]+)");
|
||||
std::smatch match;
|
||||
if (std::regex_search(version_string, match, re) && match.size() > 1) {
|
||||
return match[1].str();
|
||||
@@ -65,7 +80,6 @@ QByteArray GetFileChecksum(const std::filesystem::path& file_path) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
|
||||
UpdaterService::UpdaterService(QObject* parent) : QObject(parent) {
|
||||
network_manager = std::make_unique<QNetworkAccessManager>(this);
|
||||
InitializeSSL();
|
||||
@@ -91,22 +105,27 @@ void UpdaterService::InitializeSSL() {
|
||||
// Check if SSL is supported
|
||||
if (!QSslSocket::supportsSsl()) {
|
||||
LOG_WARNING(Frontend, "SSL support not available");
|
||||
LOG_WARNING(Frontend, "Build-time SSL version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString());
|
||||
LOG_WARNING(Frontend, "Runtime SSL version: {}", QSslSocket::sslLibraryVersionString().toStdString());
|
||||
LOG_WARNING(Frontend, "Build-time SSL version: {}",
|
||||
QSslSocket::sslLibraryBuildVersionString().toStdString());
|
||||
LOG_WARNING(Frontend, "Runtime SSL version: {}",
|
||||
QSslSocket::sslLibraryVersionString().toStdString());
|
||||
|
||||
#ifdef _WIN32
|
||||
// Try to provide helpful information about missing DLLs
|
||||
std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
|
||||
std::filesystem::path app_dir =
|
||||
std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
|
||||
std::filesystem::path crypto_dll = app_dir / "libcrypto-3-x64.dll";
|
||||
std::filesystem::path ssl_dll = app_dir / "libssl-3-x64.dll";
|
||||
|
||||
LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}", std::filesystem::exists(crypto_dll));
|
||||
LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}",
|
||||
std::filesystem::exists(crypto_dll));
|
||||
LOG_WARNING(Frontend, "libssl-3-x64.dll exists: {}", std::filesystem::exists(ssl_dll));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString());
|
||||
LOG_INFO(Frontend, "SSL library version: {}",
|
||||
QSslSocket::sslLibraryVersionString().toStdString());
|
||||
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
auto certs = QSslConfiguration::systemCaCertificates();
|
||||
@@ -126,22 +145,27 @@ void UpdaterService::CheckForUpdates() {
|
||||
return;
|
||||
}
|
||||
QSettings settings;
|
||||
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString();
|
||||
std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL;
|
||||
QString channel =
|
||||
settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString();
|
||||
std::string update_url =
|
||||
(channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL;
|
||||
LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString());
|
||||
LOG_INFO(Frontend, "Checking for updates from: {}", update_url);
|
||||
QUrl url{QString::fromStdString(update_url)};
|
||||
QNetworkRequest request{url};
|
||||
request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0"));
|
||||
request.setRawHeader("Accept", QByteArrayLiteral("application/json"));
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
current_reply = network_manager->get(request);
|
||||
connect(current_reply, &QNetworkReply::finished, this, [this, channel]() {
|
||||
if (!current_reply) return;
|
||||
if (!current_reply)
|
||||
return;
|
||||
if (current_reply->error() == QNetworkReply::NoError) {
|
||||
ParseUpdateResponse(current_reply->readAll(), channel);
|
||||
} else {
|
||||
emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString()));
|
||||
emit UpdateError(
|
||||
QStringLiteral("Update check failed: %1").arg(current_reply->errorString()));
|
||||
}
|
||||
current_reply->deleteLater();
|
||||
current_reply = nullptr;
|
||||
@@ -149,7 +173,8 @@ void UpdaterService::CheckForUpdates() {
|
||||
}
|
||||
|
||||
void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) {
|
||||
if (!QSslSocket::supportsSsl()) return;
|
||||
if (!QSslSocket::supportsSsl())
|
||||
return;
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
sslConfig.setProtocol(QSsl::SecureProtocols);
|
||||
@@ -173,7 +198,8 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) {
|
||||
|
||||
#ifdef _WIN32
|
||||
if (!CreateBackup()) {
|
||||
emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup"));
|
||||
emit UpdateCompleted(UpdateResult::PermissionError,
|
||||
QStringLiteral("Failed to create backup"));
|
||||
update_in_progress.store(false);
|
||||
return;
|
||||
}
|
||||
@@ -181,15 +207,18 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) {
|
||||
|
||||
QUrl url(QString::fromStdString(download_url));
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
current_reply = network_manager->get(request);
|
||||
connect(current_reply, &QNetworkReply::downloadProgress, this, &UpdaterService::OnDownloadProgress);
|
||||
connect(current_reply, &QNetworkReply::downloadProgress, this,
|
||||
&UpdaterService::OnDownloadProgress);
|
||||
connect(current_reply, &QNetworkReply::finished, this, &UpdaterService::OnDownloadFinished);
|
||||
connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError);
|
||||
}
|
||||
|
||||
void UpdaterService::CancelUpdate() {
|
||||
if (!update_in_progress.load()) return;
|
||||
if (!update_in_progress.load())
|
||||
return;
|
||||
cancel_requested.store(true);
|
||||
if (current_reply) {
|
||||
current_reply->abort();
|
||||
@@ -201,21 +230,12 @@ void UpdaterService::CancelUpdate() {
|
||||
|
||||
std::string UpdaterService::GetCurrentVersion() const {
|
||||
QSettings settings;
|
||||
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||
QString channel =
|
||||
settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||
|
||||
// If the user's setting is Nightly, we must ignore version.txt and only use the commit hash.
|
||||
if (channel == QStringLiteral("Nightly")) {
|
||||
std::string build_version = Common::g_build_version;
|
||||
if (!build_version.empty()) {
|
||||
std::string hash = ExtractCommitHash(build_version);
|
||||
if (!hash.empty()) {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
return ""; // Fallback if no hash is found
|
||||
}
|
||||
const std::string build_version = Common::g_build_version;
|
||||
|
||||
// Otherwise (channel is Stable), we prioritize version.txt.
|
||||
// First priority: version.txt (only relevant for Stable installations)
|
||||
std::filesystem::path search_path;
|
||||
#ifdef __linux__
|
||||
const char* appimage_path_env = qgetenv("APPIMAGE").constData();
|
||||
@@ -235,22 +255,37 @@ std::string UpdaterService::GetCurrentVersion() const {
|
||||
std::string version_from_file;
|
||||
std::getline(file, version_from_file);
|
||||
if (!version_from_file.empty()) {
|
||||
return version_from_file;
|
||||
// Trim trailing metadata/whitespace from version.txt (e.g. "1.0.0 (Release)")
|
||||
return version_from_file.substr(0, version_from_file.find_first_of(" \t\r\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for Stable channel: If version.txt is missing, use the commit hash.
|
||||
// This allows a nightly build to correctly check for a stable update.
|
||||
std::string build_version = Common::g_build_version;
|
||||
if (!build_version.empty()) {
|
||||
std::string hash = ExtractCommitHash(build_version);
|
||||
if (!hash.empty()) {
|
||||
return hash;
|
||||
// If the user's setting is Nightly, we prioritize the commit hash.
|
||||
if (channel == QStringLiteral("Nightly")) {
|
||||
if (!build_version.empty()) {
|
||||
std::string hash = ExtractCommitHash(build_version);
|
||||
if (!hash.empty()) {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise (channel is Stable), we try to extract the tag from build_version if
|
||||
// version.txt is missing. This happens when a Nightly user checks for Stable updates.
|
||||
std::string tag = ExtractVersionTag(build_version);
|
||||
if (!tag.empty()) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
// Common fallback: try to extract a hash if we haven't found a tag yet,
|
||||
// otherwise just return the full build version.
|
||||
std::string hash_fallback = ExtractCommitHash(build_version);
|
||||
if (!hash_fallback.empty()) {
|
||||
return hash_fallback;
|
||||
}
|
||||
|
||||
return build_version;
|
||||
}
|
||||
|
||||
bool UpdaterService::IsUpdateInProgress() const {
|
||||
@@ -270,14 +305,17 @@ void UpdaterService::OnDownloadFinished() {
|
||||
|
||||
QByteArray downloaded_data = current_reply->readAll();
|
||||
QSettings settings;
|
||||
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||
QString channel =
|
||||
settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||
|
||||
#if defined(_WIN32)
|
||||
QString filename = QStringLiteral("citron_update_%1.zip").arg(QString::fromStdString(current_update_info.version));
|
||||
QString filename = QStringLiteral("citron_update_%1.zip")
|
||||
.arg(QString::fromStdString(current_update_info.version));
|
||||
std::filesystem::path download_path = temp_download_path / filename.toStdString();
|
||||
QFile file(QString::fromStdString(download_path.string()));
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
emit UpdateCompleted(UpdateResult::Failed, QStringLiteral("Failed to save downloaded file"));
|
||||
emit UpdateCompleted(UpdateResult::Failed,
|
||||
QStringLiteral("Failed to save downloaded file"));
|
||||
update_in_progress.store(false);
|
||||
return;
|
||||
}
|
||||
@@ -293,7 +331,8 @@ void UpdaterService::OnDownloadFinished() {
|
||||
emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive..."));
|
||||
std::filesystem::path extract_path = temp_download_path / "extracted";
|
||||
if (!ExtractArchive(download_path, extract_path)) {
|
||||
emit UpdateCompleted(UpdateResult::ExtractionError, QStringLiteral("Failed to extract update archive"));
|
||||
emit UpdateCompleted(UpdateResult::ExtractionError,
|
||||
QStringLiteral("Failed to extract update archive"));
|
||||
update_in_progress.store(false);
|
||||
return;
|
||||
}
|
||||
@@ -305,7 +344,9 @@ void UpdaterService::OnDownloadFinished() {
|
||||
return;
|
||||
}
|
||||
emit UpdateInstallProgress(100, QStringLiteral("Update completed successfully!"));
|
||||
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update installed successfully. Please restart the application."));
|
||||
emit UpdateCompleted(
|
||||
UpdateResult::Success,
|
||||
QStringLiteral("Update installed successfully. Please restart the application."));
|
||||
update_in_progress.store(false);
|
||||
CleanupFiles();
|
||||
});
|
||||
@@ -344,13 +385,17 @@ void UpdaterService::OnDownloadFinished() {
|
||||
} else {
|
||||
// Create the backup copy of the old AppImage
|
||||
std::string current_version = GetCurrentVersion();
|
||||
std::string backup_filename = "citron-backup-" + (current_version.empty() ? "unknown" : current_version) + ".AppImage";
|
||||
std::string backup_filename = "citron-backup-" +
|
||||
(current_version.empty() ? "unknown" : current_version) +
|
||||
".AppImage";
|
||||
std::filesystem::path backup_filepath = backup_dir / backup_filename;
|
||||
std::filesystem::copy_file(original_appimage_path, backup_filepath, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
std::filesystem::copy_file(original_appimage_path, backup_filepath,
|
||||
std::filesystem::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message());
|
||||
} else {
|
||||
LOG_INFO(Frontend, "Created backup of old AppImage at: {}", backup_filepath.string());
|
||||
LOG_INFO(Frontend, "Created backup of old AppImage at: {}",
|
||||
backup_filepath.string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,9 +410,10 @@ void UpdaterService::OnDownloadFinished() {
|
||||
new_file.write(downloaded_data);
|
||||
new_file.close();
|
||||
|
||||
if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner |
|
||||
QFileDevice::ReadGroup | QFileDevice::ExeGroup |
|
||||
QFileDevice::ReadOther | QFileDevice::ExeOther)) {
|
||||
if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
|
||||
QFileDevice::ExeOwner | QFileDevice::ReadGroup |
|
||||
QFileDevice::ExeGroup | QFileDevice::ReadOther |
|
||||
QFileDevice::ExeOther)) {
|
||||
emit UpdateError(QStringLiteral("Failed to make the new AppImage executable."));
|
||||
std::filesystem::remove(new_appimage_path, ec);
|
||||
update_in_progress.store(false);
|
||||
@@ -397,7 +443,8 @@ void UpdaterService::OnDownloadFinished() {
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend, "AppImage updated successfully.");
|
||||
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application."));
|
||||
emit UpdateCompleted(UpdateResult::Success,
|
||||
QStringLiteral("Update successful. Please restart the application."));
|
||||
update_in_progress.store(false);
|
||||
#endif
|
||||
}
|
||||
@@ -430,42 +477,50 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response, const QStri
|
||||
if (channel == QStringLiteral("Stable")) {
|
||||
latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString();
|
||||
} else {
|
||||
latest_version = ExtractCommitHash(release_obj.value(QStringLiteral("name")).toString().toStdString());
|
||||
latest_version = ExtractCommitHash(
|
||||
release_obj.value(QStringLiteral("name")).toString().toStdString());
|
||||
}
|
||||
|
||||
if (latest_version.empty()) continue;
|
||||
if (latest_version.empty())
|
||||
continue;
|
||||
|
||||
UpdateInfo update_info;
|
||||
update_info.version = latest_version;
|
||||
update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString();
|
||||
update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString();
|
||||
update_info.release_date =
|
||||
release_obj.value(QStringLiteral("published_at")).toString().toStdString();
|
||||
|
||||
QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray();
|
||||
for (const QJsonValue& asset_value : assets) {
|
||||
QJsonObject asset_obj = asset_value.toObject();
|
||||
QString asset_name = asset_obj.value(QStringLiteral("name")).toString();
|
||||
|
||||
#if defined(__linux__)
|
||||
#if defined(__linux__)
|
||||
if (asset_name.endsWith(QStringLiteral(".AppImage"))) {
|
||||
DownloadOption option;
|
||||
option.name = asset_name.toStdString();
|
||||
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString();
|
||||
option.url = asset_obj.value(QStringLiteral("browser_download_url"))
|
||||
.toString()
|
||||
.toStdString();
|
||||
update_info.download_options.push_back(option);
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
#elif defined(_WIN32)
|
||||
// For Windows, find the .zip file but explicitly skip PGO builds.
|
||||
if (asset_name.endsWith(QStringLiteral(".zip")) && !asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) {
|
||||
if (asset_name.endsWith(QStringLiteral(".zip")) &&
|
||||
!asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) {
|
||||
DownloadOption option;
|
||||
option.name = asset_name.toStdString();
|
||||
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString();
|
||||
option.url = asset_obj.value(QStringLiteral("browser_download_url"))
|
||||
.toString()
|
||||
.toStdString();
|
||||
update_info.download_options.push_back(option);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!update_info.download_options.empty()) {
|
||||
update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version);
|
||||
update_info.is_newer_version =
|
||||
CompareVersions(GetCurrentVersion(), update_info.version);
|
||||
current_update_info = update_info;
|
||||
emit UpdateCheckCompleted(update_info.is_newer_version, update_info);
|
||||
return;
|
||||
@@ -487,28 +542,34 @@ bool UpdaterService::CompareVersions(const std::string& current, const std::stri
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) {
|
||||
bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path,
|
||||
const std::filesystem::path& extract_path) {
|
||||
#ifdef CITRON_ENABLE_LIBARCHIVE
|
||||
struct archive* a = archive_read_new();
|
||||
struct archive* ext = archive_write_disk_new();
|
||||
if (!a || !ext) return false;
|
||||
if (!a || !ext)
|
||||
return false;
|
||||
archive_read_support_format_7zip(a);
|
||||
archive_read_support_filter_all(a);
|
||||
archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM);
|
||||
archive_write_disk_set_standard_lookup(ext);
|
||||
if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) return false;
|
||||
if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK)
|
||||
return false;
|
||||
EnsureDirectoryExists(extract_path);
|
||||
struct archive_entry* entry;
|
||||
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
||||
if (cancel_requested.load()) break;
|
||||
if (cancel_requested.load())
|
||||
break;
|
||||
std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry);
|
||||
archive_entry_set_pathname(entry, entry_path.string().c_str());
|
||||
if (archive_write_header(ext, entry) != ARCHIVE_OK) continue;
|
||||
if (archive_write_header(ext, entry) != ARCHIVE_OK)
|
||||
continue;
|
||||
const void* buff;
|
||||
size_t size;
|
||||
la_int64_t offset;
|
||||
while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) {
|
||||
if (cancel_requested.load()) break;
|
||||
if (cancel_requested.load())
|
||||
break;
|
||||
archive_write_data_block(ext, buff, size, offset);
|
||||
}
|
||||
archive_write_finish_entry(ext);
|
||||
@@ -524,12 +585,18 @@ bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, c
|
||||
}
|
||||
|
||||
#if !defined(CITRON_ENABLE_LIBARCHIVE)
|
||||
bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) {
|
||||
bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path,
|
||||
const std::filesystem::path& extract_path) {
|
||||
EnsureDirectoryExists(extract_path);
|
||||
std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y";
|
||||
if (std::system(sevenzip_cmd.c_str()) == 0) return true;
|
||||
std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + archive_path.string() + "\\\" -DestinationPath \\\"" + extract_path.string() + "\\\" -Force\"";
|
||||
if (std::system(powershell_cmd.c_str()) == 0) return true;
|
||||
std::string sevenzip_cmd =
|
||||
"7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y";
|
||||
if (std::system(sevenzip_cmd.c_str()) == 0)
|
||||
return true;
|
||||
std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" +
|
||||
archive_path.string() + "\\\" -DestinationPath \\\"" +
|
||||
extract_path.string() + "\\\" -Force\"";
|
||||
if (std::system(powershell_cmd.c_str()) == 0)
|
||||
return true;
|
||||
LOG_ERROR(Frontend, "Failed to extract archive automatically.");
|
||||
return false;
|
||||
}
|
||||
@@ -548,12 +615,15 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) {
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
EnsureDirectoryExists(staging_path);
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) {
|
||||
if (cancel_requested.load()) return false;
|
||||
if (cancel_requested.load())
|
||||
return false;
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), source_path);
|
||||
std::filesystem::path relative_path =
|
||||
std::filesystem::relative(entry.path(), source_path);
|
||||
std::filesystem::path staging_dest = staging_path / relative_path;
|
||||
std::filesystem::create_directories(staging_dest.parent_path());
|
||||
std::filesystem::copy_file(entry.path(), staging_dest, std::filesystem::copy_options::overwrite_existing);
|
||||
std::filesystem::copy_file(entry.path(), staging_dest,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
}
|
||||
}
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
@@ -585,14 +655,16 @@ bool UpdaterService::CreateBackup() {
|
||||
std::filesystem::remove_all(backup_dir);
|
||||
}
|
||||
std::filesystem::create_directories(backup_dir);
|
||||
std::vector<std::string> backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", "*.pdb"};
|
||||
std::vector<std::string> backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll",
|
||||
"*.pdb"};
|
||||
for (const auto& entry : std::filesystem::directory_iterator(app_directory)) {
|
||||
if (entry.is_regular_file()) {
|
||||
std::string filename = entry.path().filename().string();
|
||||
std::string extension = entry.path().extension().string();
|
||||
bool should_backup = false;
|
||||
for (const auto& pattern : backup_patterns) {
|
||||
if (pattern == filename || (pattern.starts_with("*") && pattern.substr(1) == extension)) {
|
||||
if (pattern == filename ||
|
||||
(pattern.starts_with("*") && pattern.substr(1) == extension)) {
|
||||
should_backup = true;
|
||||
break;
|
||||
}
|
||||
@@ -613,11 +685,13 @@ bool UpdaterService::CreateBackup() {
|
||||
bool UpdaterService::RestoreBackup() {
|
||||
try {
|
||||
std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion());
|
||||
if (!std::filesystem::exists(backup_dir)) return false;
|
||||
if (!std::filesystem::exists(backup_dir))
|
||||
return false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(backup_dir)) {
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path dest_path = app_directory / entry.path().filename();
|
||||
std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing);
|
||||
std::filesystem::copy_file(entry.path(), dest_path,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
}
|
||||
}
|
||||
LOG_INFO(Frontend, "Backup restored successfully");
|
||||
@@ -648,9 +722,15 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
|
||||
std::string app_path_str = app_directory.string();
|
||||
std::string exe_path_str = (app_directory / "citron.exe").string();
|
||||
|
||||
for (auto& ch : staging_path_str) if (ch == '/') ch = '\\';
|
||||
for (auto& ch : app_path_str) if (ch == '/') ch = '\\';
|
||||
for (auto& ch : exe_path_str) if (ch == '/') ch = '\\';
|
||||
for (auto& ch : staging_path_str)
|
||||
if (ch == '/')
|
||||
ch = '\\';
|
||||
for (auto& ch : app_path_str)
|
||||
if (ch == '/')
|
||||
ch = '\\';
|
||||
for (auto& ch : exe_path_str)
|
||||
if (ch == '/')
|
||||
ch = '\\';
|
||||
|
||||
script << "@echo off\n";
|
||||
script << "setlocal enabledelayedexpansion\n";
|
||||
@@ -665,7 +745,8 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
|
||||
script << "if not errorlevel 1 (\n";
|
||||
script << " set /a wait_count+=1\n";
|
||||
script << " if !wait_count! gtr 60 (\n";
|
||||
script << " echo Warning: Citron process still running after 60 seconds, proceeding anyway...\n";
|
||||
script << " echo Warning: Citron process still running after 60 seconds, proceeding "
|
||||
"anyway...\n";
|
||||
script << " goto wait_done\n";
|
||||
script << " )\n";
|
||||
script << " timeout /t 1 /nobreak >nul\n";
|
||||
@@ -678,14 +759,17 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
|
||||
// Remove read-only attributes from all files in the destination directory
|
||||
script << "echo Removing read-only attributes from existing files...\n";
|
||||
script << "attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n";
|
||||
script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str << "\\citron.exe\" >nul 2>&1\n";
|
||||
script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str << "\\citron_cmd.exe\" >nul 2>&1\n\n";
|
||||
script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str
|
||||
<< "\\citron.exe\" >nul 2>&1\n";
|
||||
script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str
|
||||
<< "\\citron_cmd.exe\" >nul 2>&1\n\n";
|
||||
|
||||
// Use robocopy for more reliable copying (available on Windows Vista+)
|
||||
script << "echo Copying update files...\n";
|
||||
script << "set /a copy_retries=0\n";
|
||||
script << ":copy_loop\n";
|
||||
script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str << "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n";
|
||||
script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str
|
||||
<< "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n";
|
||||
script << "set /a robocopy_exit=!errorlevel!\n";
|
||||
script << "REM Robocopy returns 0-7 for success, 8+ for errors\n";
|
||||
script << "if !robocopy_exit! geq 8 (\n";
|
||||
@@ -786,10 +870,13 @@ bool UpdaterService::LaunchUpdateHelper() {
|
||||
bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments);
|
||||
|
||||
if (launched) {
|
||||
LOG_INFO(Frontend, "Update helper script launched successfully from: {}", script_path.string());
|
||||
LOG_INFO(Frontend, "Update helper script launched successfully from: {}",
|
||||
script_path.string());
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Failed to launch update helper script. QProcess::startDetached returned false");
|
||||
LOG_ERROR(
|
||||
Frontend,
|
||||
"Failed to launch update helper script. QProcess::startDetached returned false");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
@@ -808,14 +895,16 @@ bool UpdaterService::CleanupFiles() {
|
||||
std::vector<std::filesystem::path> backup_dirs;
|
||||
if (std::filesystem::exists(backup_path)) {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(backup_path)) {
|
||||
if (entry.is_directory() && entry.path().filename().string().starts_with("backup_")) {
|
||||
if (entry.is_directory() &&
|
||||
entry.path().filename().string().starts_with("backup_")) {
|
||||
backup_dirs.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (backup_dirs.size() > 3) {
|
||||
std::sort(backup_dirs.begin(), backup_dirs.end(),
|
||||
[](const auto& a, const auto& b) { return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); });
|
||||
std::sort(backup_dirs.begin(), backup_dirs.end(), [](const auto& a, const auto& b) {
|
||||
return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b);
|
||||
});
|
||||
for (size_t i = 3; i < backup_dirs.size(); ++i) {
|
||||
std::filesystem::remove_all(backup_dirs[i]);
|
||||
}
|
||||
@@ -829,7 +918,9 @@ bool UpdaterService::CleanupFiles() {
|
||||
}
|
||||
|
||||
std::filesystem::path UpdaterService::GetTempDirectory() const {
|
||||
return std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater";
|
||||
return std::filesystem::path(
|
||||
QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) /
|
||||
"citron_updater";
|
||||
}
|
||||
|
||||
std::filesystem::path UpdaterService::GetApplicationDirectory() const {
|
||||
@@ -856,7 +947,8 @@ bool UpdaterService::HasStagedUpdate(const std::filesystem::path& app_directory)
|
||||
#ifdef _WIN32
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) && std::filesystem::is_directory(staging_path);
|
||||
return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) &&
|
||||
std::filesystem::is_directory(staging_path);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -867,15 +959,19 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
|
||||
try {
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file)) return false;
|
||||
if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file))
|
||||
return false;
|
||||
LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string());
|
||||
std::filesystem::path backup_path_dir = app_directory / "backup_before_update";
|
||||
if (std::filesystem::exists(backup_path_dir)) std::filesystem::remove_all(backup_path_dir);
|
||||
if (std::filesystem::exists(backup_path_dir))
|
||||
std::filesystem::remove_all(backup_path_dir);
|
||||
std::filesystem::create_directories(backup_path_dir);
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) {
|
||||
if (entry.path().filename() == "update_manifest.txt") continue;
|
||||
if (entry.path().filename() == "update_manifest.txt")
|
||||
continue;
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), staging_path);
|
||||
std::filesystem::path relative_path =
|
||||
std::filesystem::relative(entry.path(), staging_path);
|
||||
std::filesystem::path dest_path = app_directory / relative_path;
|
||||
if (std::filesystem::exists(dest_path)) {
|
||||
std::filesystem::path backup_dest = backup_path_dir / relative_path;
|
||||
@@ -883,7 +979,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
|
||||
std::filesystem::copy_file(dest_path, backup_dest);
|
||||
}
|
||||
std::filesystem::create_directories(dest_path.parent_path());
|
||||
std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing);
|
||||
std::filesystem::copy_file(entry.path(), dest_path,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
}
|
||||
}
|
||||
std::ifstream manifest(manifest_file);
|
||||
@@ -897,7 +994,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
|
||||
if (!version.empty()) {
|
||||
std::filesystem::path version_file = app_directory / "version.txt";
|
||||
std::ofstream vfile(version_file);
|
||||
if (vfile.is_open()) vfile << version;
|
||||
if (vfile.is_open())
|
||||
vfile << version;
|
||||
}
|
||||
std::filesystem::remove_all(staging_path);
|
||||
LOG_INFO(Frontend, "Update applied successfully. Version: {}", version);
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <QNetworkReply>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
|
||||
|
||||
#include <QString>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QString>
|
||||
|
||||
|
||||
namespace Updater {
|
||||
|
||||
// Declarations for helper functions
|
||||
QString FormatDateTimeString(const std::string& iso_string);
|
||||
std::string ExtractCommitHash(const std::string& version_string);
|
||||
std::string ExtractVersionTag(const std::string& version_string);
|
||||
QByteArray GetFileChecksum(const std::filesystem::path& file_path);
|
||||
|
||||
struct DownloadOption {
|
||||
@@ -39,7 +42,16 @@ class UpdaterService : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable };
|
||||
enum class UpdateResult {
|
||||
Success,
|
||||
Failed,
|
||||
Cancelled,
|
||||
NetworkError,
|
||||
ExtractionError,
|
||||
PermissionError,
|
||||
InvalidArchive,
|
||||
NoUpdateAvailable
|
||||
};
|
||||
|
||||
explicit UpdaterService(QObject* parent = nullptr);
|
||||
~UpdaterService() override;
|
||||
@@ -53,9 +65,9 @@ public:
|
||||
static bool HasStagedUpdate(const std::filesystem::path& app_directory);
|
||||
static bool ApplyStagedUpdate(const std::filesystem::path& app_directory);
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef _WIN32
|
||||
bool LaunchUpdateHelper();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info);
|
||||
@@ -75,16 +87,18 @@ private:
|
||||
void ParseUpdateResponse(const QByteArray& response, const QString& channel);
|
||||
bool CompareVersions(const std::string& current, const std::string& latest) const;
|
||||
|
||||
#ifdef _WIN32
|
||||
bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path);
|
||||
#ifndef CITRON_ENABLE_LIBARCHIVE
|
||||
bool ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
bool ExtractArchive(const std::filesystem::path& archive_path,
|
||||
const std::filesystem::path& extract_path);
|
||||
#ifndef CITRON_ENABLE_LIBARCHIVE
|
||||
bool ExtractArchiveWindows(const std::filesystem::path& archive_path,
|
||||
const std::filesystem::path& extract_path);
|
||||
#endif
|
||||
bool InstallUpdate(const std::filesystem::path& update_path);
|
||||
bool CreateBackup();
|
||||
bool RestoreBackup();
|
||||
bool CreateUpdateHelperScript(const std::filesystem::path& staging_path);
|
||||
#endif
|
||||
#endif
|
||||
bool CleanupFiles();
|
||||
std::filesystem::path GetTempDirectory() const;
|
||||
std::filesystem::path GetApplicationDirectory() const;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QObject>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ContentProviderEntry> ManualContentProvider::ListEntriesFilter(
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<u32> out_license, Out<s64> 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<u32>(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<ProfileBase>();
|
||||
@@ -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<ISessionObject>(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<u32>();
|
||||
const auto uuid = rp.PopRaw<Common::UUID>();
|
||||
|
||||
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<IOAuthProcedureForUserRegistration>(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<IOAuthProcedureForExternalNsa>(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<IOAuthProcedureForExternalNsa>(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> module_,
|
||||
std::shared_ptr<ProfileManager> 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<ACC_AA>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:e",
|
||||
std::make_shared<ACC_E>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:e:u1",
|
||||
std::make_shared<ACC_E_U1>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:e:u2",
|
||||
std::make_shared<ACC_E_U2>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService(
|
||||
"acc:e:u1", std::make_shared<ACC_E_U1>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService(
|
||||
"acc:e:u2", std::make_shared<ACC_E_U2>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:su",
|
||||
std::make_shared<ACC_SU>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:u0",
|
||||
std::make_shared<ACC_U0>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("acc:u1",
|
||||
std::make_shared<ACC_U1>(module, profile_manager, system));
|
||||
server_manager->RegisterNamedService("dauth:0",
|
||||
std::make_shared<DAUTH_0>(system));
|
||||
server_manager->RegisterNamedService("dauth:0", std::make_shared<DAUTH_0>(system));
|
||||
|
||||
ServerManager::RunServer(std::move(server_manager));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u64>(AppletProgramId::OverlayDisplay), 0, 0)) {
|
||||
auto overlay_applet =
|
||||
std::make_shared<Applet>(m_system, std::move(overlay_process), false);
|
||||
overlay_applet->program_id = static_cast<u64>(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<Applet>(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?
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
#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<void()> callback);
|
||||
|
||||
@@ -59,6 +64,8 @@ private:
|
||||
|
||||
FrontendAppletParameters m_pending_parameters{};
|
||||
std::unique_ptr<Process> m_pending_process{};
|
||||
|
||||
FileSys::ManualContentProvider m_manual_provider;
|
||||
};
|
||||
|
||||
} // namespace Service::AM
|
||||
|
||||
@@ -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<u32>(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<u32>(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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,6 +46,7 @@ private:
|
||||
enum class PhotoViewerAppletMode : u8 {
|
||||
CurrentApp = 0,
|
||||
AllApps = 1,
|
||||
ShowAllFiles = 2,
|
||||
};
|
||||
|
||||
class PhotoViewer final : public FrontendApplet {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@ enum class ShimKind : u32 {
|
||||
Web = 5,
|
||||
Wifi = 6,
|
||||
Lobby = 7,
|
||||
Unknown8 = 8,
|
||||
};
|
||||
|
||||
enum class WebExitReason : u32 {
|
||||
|
||||
@@ -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<SharedPointer<ILibraryAppletProxy>> out_library_applet_proxy, ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle,
|
||||
InLargeData<AppletAttribute, BufferAttr_HipcMapAlias> attribute) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
InCopyHandle<Kernel::KProcess> process_handle, AppletAttribute attribute) {
|
||||
LOG_WARNING(Service_AM, "called");
|
||||
|
||||
if (const auto applet = this->GetAppletFromProcessId(pid); applet) {
|
||||
*out_library_applet_proxy = std::make_shared<ILibraryAppletProxy>(
|
||||
@@ -104,8 +104,7 @@ std::shared_ptr<Applet> IAllSystemAppletProxiesService::GetAppletFromProcessId(
|
||||
|
||||
Result IAllSystemAppletProxiesService::OpenOverlayAppletProxy(
|
||||
Out<SharedPointer<IOverlayAppletProxy>> out_overlay_applet_proxy, ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle,
|
||||
InLargeData<AppletAttribute, BufferAttr_HipcMapAlias> attribute) {
|
||||
InCopyHandle<Kernel::KProcess> process_handle, AppletAttribute attribute) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
if (const auto applet = GetAppletFromProcessId(pid)) {
|
||||
*out_overlay_applet_proxy = std::make_shared<IOverlayAppletProxy>(
|
||||
@@ -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<ILibraryAppletCreator>(system, applet, m_window_system);
|
||||
*out_library_applet_creator =
|
||||
std::make_shared<ILibraryAppletCreator>(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<SharedPointer<IAppletAlternativeFunctions>> out_applet_alternative_functions) {
|
||||
LOG_WARNING(Service_AM, "called");
|
||||
*out_applet_alternative_functions =
|
||||
std::make_shared<IAppletAlternativeFunctions>(system, m_window_system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Kernel::KProcess> process_handle);
|
||||
Result OpenHomeMenuProxy(Out<SharedPointer<ISystemAppletProxy>> out_system_applet_proxy,
|
||||
ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle);
|
||||
ClientProcessId pid, InCopyHandle<Kernel::KProcess> process_handle);
|
||||
Result OpenLibraryAppletProxy(Out<SharedPointer<ILibraryAppletProxy>> out_library_applet_proxy,
|
||||
ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle,
|
||||
InLargeData<AppletAttribute, BufferAttr_HipcMapAlias> attribute);
|
||||
AppletAttribute attribute);
|
||||
Result OpenLibraryAppletProxyOld(
|
||||
Out<SharedPointer<ILibraryAppletProxy>> out_library_applet_proxy, ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle);
|
||||
Result OpenOverlayAppletProxy(Out<SharedPointer<IOverlayAppletProxy>> out_overlay_applet_proxy,
|
||||
ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle,
|
||||
InLargeData<AppletAttribute, BufferAttr_HipcMapAlias> attribute);
|
||||
Result OpenSystemApplicationProxy(Out<SharedPointer<ISystemApplicationProxy>> out_system_application_proxy,
|
||||
ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle);
|
||||
Result CreateSelfLibraryAppletCreatorForDevelop(Out<SharedPointer<ILibraryAppletCreator>> out_library_applet_creator,
|
||||
ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle);
|
||||
AppletAttribute attribute);
|
||||
Result OpenSystemApplicationProxy(
|
||||
Out<SharedPointer<ISystemApplicationProxy>> out_system_application_proxy,
|
||||
ClientProcessId pid, InCopyHandle<Kernel::KProcess> process_handle);
|
||||
Result CreateSelfLibraryAppletCreatorForDevelop(
|
||||
Out<SharedPointer<ILibraryAppletCreator>> out_library_applet_creator, ClientProcessId pid,
|
||||
InCopyHandle<Kernel::KProcess> process_handle);
|
||||
Result GetSystemAppletControllerForDebug();
|
||||
Result GetSystemProcessCommonFunctions(
|
||||
Out<SharedPointer<ISystemProcessCommonFunctions>> out_system_process_common_functions);
|
||||
Result Cmd460();
|
||||
Result GetAppletAlternativeFunctions(
|
||||
Out<SharedPointer<IAppletAlternativeFunctions>> out_applet_alternative_functions);
|
||||
Result GetDebugFunctions(Out<SharedPointer<IDebugFunctions>> out_debug_functions);
|
||||
|
||||
private:
|
||||
|
||||
@@ -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<u64> out_target) {
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||
*out_target = 0; // Return 0? Or some applet ID?
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace Service::AM
|
||||
@@ -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<IAppletAlternativeFunctions> {
|
||||
public:
|
||||
explicit IAppletAlternativeFunctions(Core::System& system_, WindowSystem& window_system);
|
||||
~IAppletAlternativeFunctions() override;
|
||||
|
||||
private:
|
||||
Result GetAlternativeTarget(Out<u64> out_target);
|
||||
|
||||
WindowSystem& m_window_system;
|
||||
};
|
||||
|
||||
} // namespace Service::AM
|
||||
@@ -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<f32>{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};
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
private:
|
||||
Result SetHomeButtonDoubleClickEnabled(bool home_button_double_click_enabled);
|
||||
Result GetHomeButtonDoubleClickEnabled(Out<bool> out_home_button_double_click_enabled);
|
||||
Result SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height);
|
||||
Result SetCpuBoostRequestPriority(s32 priority);
|
||||
Result GetCurrentApplicationId(Out<u64> out_application_id);
|
||||
Result IsSystemAppletHomeMenu(Out<bool> out_is_home_menu);
|
||||
|
||||
@@ -39,7 +39,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, std::shared_ptr<Ap
|
||||
{30, nullptr, "GetHomeButtonReaderLockAccessor"},
|
||||
{31, D<&ICommonStateGetter::GetReaderLockAccessorEx>, "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<u32> out_operation_mod
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ICommonStateGetter::GetCradleFwVersion(Out<u32> out_major, Out<u32> out_minor,
|
||||
Out<u32> out_micro) {
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||
|
||||
*out_major = 0;
|
||||
*out_minor = 0;
|
||||
*out_micro = 0;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ICommonStateGetter::GetAppletLaunchedHistory(
|
||||
Out<s32> out_count, OutArray<AppletId, BufferAttr_HipcMapAlias> out_applet_ids) {
|
||||
LOG_INFO(Service_AM, "called");
|
||||
|
||||
@@ -37,6 +37,7 @@ private:
|
||||
Result GetDefaultDisplayResolutionChangeEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
|
||||
Result GetOperationMode(Out<OperationMode> out_operation_mode);
|
||||
Result GetPerformanceMode(Out<APM::PerformanceMode> out_performance_mode);
|
||||
Result GetCradleFwVersion(Out<u32> out_major, Out<u32> out_minor, Out<u32> out_micro);
|
||||
Result GetBootMode(Out<PM::SystemBootMode> out_boot_mode);
|
||||
Result IsVrModeEnabled(Out<bool> out_is_vr_mode_enabled);
|
||||
Result SetVrModeEnabled(bool is_vr_mode_enabled);
|
||||
|
||||
@@ -106,15 +106,11 @@ std::shared_ptr<ILibraryAppletAccessor> 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<ILibraryAppletAccessor> CreateFrontendApplet(Core::System& syste
|
||||
|
||||
ILibraryAppletCreator::ILibraryAppletCreator(Core::System& system_, std::shared_ptr<Applet> 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"},
|
||||
|
||||
@@ -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<IOverlayAppletFunctions> {
|
||||
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> 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<SharedPointer<IProcessWindingController>> out_process_winding_controller) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_process_winding_controller = std::make_shared<IProcessWindingController>(system, m_applet);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetLibraryAppletCreator(
|
||||
Out<SharedPointer<ILibraryAppletCreator>> out_library_applet_creator) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
@@ -115,21 +87,6 @@ Result IOverlayAppletProxy::GetLibraryAppletCreator(
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetOverlayAppletFunctions(
|
||||
Out<SharedPointer<IOverlayAppletFunctions>> out_overlay_applet_functions) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_overlay_applet_functions = std::make_shared<IOverlayAppletFunctions>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetHomeMenuFunctions(
|
||||
Out<SharedPointer<IHomeMenuFunctions>> out_home_menu_functions) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_home_menu_functions =
|
||||
std::make_shared<IHomeMenuFunctions>(system, m_applet, m_window_system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetGlobalStateController(
|
||||
Out<SharedPointer<IGlobalStateController>> out_global_state_controller) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
@@ -144,4 +101,25 @@ Result IOverlayAppletProxy::GetDebugFunctions(
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace Service::AM
|
||||
Result IOverlayAppletProxy::GetProcessWindingController(
|
||||
Out<SharedPointer<IProcessWindingController>> out_process_winding_controller) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_process_winding_controller = std::make_shared<IProcessWindingController>(system, m_applet);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetOverlayFunctions(
|
||||
Out<SharedPointer<IOverlayFunctions>> out_overlay_functions) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_overlay_functions = std::make_shared<IOverlayFunctions>(system, m_applet);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IOverlayAppletProxy::GetAppletCommonFunctions(
|
||||
Out<SharedPointer<IAppletCommonFunctions>> out_applet_common_functions) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
*out_applet_common_functions = std::make_shared<IAppletCommonFunctions>(system, m_applet);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace Service::AM
|
||||
|
||||
@@ -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<IOverlayAppletProxy> {
|
||||
public:
|
||||
@@ -38,11 +42,15 @@ private:
|
||||
Result GetWindowController(Out<SharedPointer<IWindowController>> out_window_controller);
|
||||
Result GetAudioController(Out<SharedPointer<IAudioController>> out_audio_controller);
|
||||
Result GetDisplayController(Out<SharedPointer<IDisplayController>> out_display_controller);
|
||||
Result GetProcessWindingController(Out<SharedPointer<IProcessWindingController>> out_process_winding_controller);
|
||||
Result GetLibraryAppletCreator(Out<SharedPointer<ILibraryAppletCreator>> out_library_applet_creator);
|
||||
Result GetOverlayAppletFunctions(Out<SharedPointer<IOverlayAppletFunctions>> out_overlay_applet_functions);
|
||||
Result GetHomeMenuFunctions(Out<SharedPointer<IHomeMenuFunctions>> out_home_menu_functions);
|
||||
Result GetGlobalStateController(Out<SharedPointer<IGlobalStateController>> out_global_state_controller);
|
||||
Result GetProcessWindingController(
|
||||
Out<SharedPointer<IProcessWindingController>> out_process_winding_controller);
|
||||
Result GetLibraryAppletCreator(
|
||||
Out<SharedPointer<ILibraryAppletCreator>> out_library_applet_creator);
|
||||
Result GetOverlayFunctions(Out<SharedPointer<IOverlayFunctions>> out_overlay_functions);
|
||||
Result GetAppletCommonFunctions(
|
||||
Out<SharedPointer<IAppletCommonFunctions>> out_applet_common_functions);
|
||||
Result GetGlobalStateController(
|
||||
Out<SharedPointer<IGlobalStateController>> out_global_state_controller);
|
||||
Result GetDebugFunctions(Out<SharedPointer<IDebugFunctions>> out_debug_functions);
|
||||
|
||||
WindowSystem& m_window_system;
|
||||
@@ -50,4 +58,4 @@ private:
|
||||
const std::shared_ptr<Applet> m_applet;
|
||||
};
|
||||
|
||||
} // namespace Service::AM
|
||||
} // namespace Service::AM
|
||||
|
||||
128
src/core/hle/service/am/service/overlay_functions.cpp
Normal file
128
src/core/hle/service/am/service/overlay_functions.cpp
Normal file
@@ -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> 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<u64> out_application_id) {
|
||||
LOG_DEBUG(Service_AM, "called");
|
||||
|
||||
std::shared_ptr<Applet> 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<u32>(target_applet->applet_id), target_applet->program_id,
|
||||
static_cast<u32>(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<bool> 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
|
||||
30
src/core/hle/service/am/service/overlay_functions.h
Normal file
30
src/core/hle/service/am/service/overlay_functions.h
Normal file
@@ -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<IOverlayFunctions> {
|
||||
public:
|
||||
explicit IOverlayFunctions(Core::System& system_, std::shared_ptr<Applet> applet_);
|
||||
~IOverlayFunctions() override;
|
||||
|
||||
private:
|
||||
Result BeginToWatchShortHomeButtonMessage();
|
||||
Result EndToWatchShortHomeButtonMessage();
|
||||
Result GetApplicationIdForLogo(Out<u64> out_application_id);
|
||||
Result SetAutoSleepTimeAndDimmingTimeEnabled(bool enabled);
|
||||
Result IsHealthWarningRequired(Out<bool> is_required);
|
||||
Result SetHandlingHomeButtonShortPressedEnabled(bool enabled);
|
||||
Result Unknown70();
|
||||
|
||||
private:
|
||||
const std::shared_ptr<Applet> m_applet;
|
||||
};
|
||||
|
||||
} // namespace Service::AM
|
||||
@@ -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> 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<Applet> WindowSystem::GetOverlayDisplayApplet() {
|
||||
return overlay_display_applet;
|
||||
}
|
||||
|
||||
} // namespace Service::AM
|
||||
|
||||
@@ -35,11 +35,13 @@ public:
|
||||
public:
|
||||
void SetEventObserver(EventObserver* event_observer);
|
||||
void Update();
|
||||
void RequestUpdate();
|
||||
|
||||
public:
|
||||
void TrackApplet(std::shared_ptr<Applet> applet, bool is_application);
|
||||
std::shared_ptr<Applet> GetByAppletResourceUserId(u64 aruid);
|
||||
std::shared_ptr<Applet> GetMainApplet();
|
||||
std::shared_ptr<Applet> GetOverlayDisplayApplet();
|
||||
|
||||
public:
|
||||
void RequestHomeMenuToGetForeground();
|
||||
@@ -74,6 +76,9 @@ private:
|
||||
// Lock.
|
||||
std::mutex m_lock{};
|
||||
|
||||
// Overlay Display Applet.
|
||||
std::shared_ptr<Applet> overlay_display_applet;
|
||||
|
||||
// Home menu state.
|
||||
bool m_home_menu_foreground_locked{};
|
||||
Applet* m_foreground_requested_applet{};
|
||||
|
||||
@@ -45,6 +45,11 @@ static std::vector<u64> 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<u32> out_count, ClientProcess
|
||||
Result IAddOnContentManager::ListAddOnContent(Out<u32> out_count,
|
||||
OutBuffer<BufferAttr_HipcMapAlias> 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<u32> 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<u32>(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<u32>(std::min<size_t>(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<u32> 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<u32>(
|
||||
std::count_if(add_on_content.begin(), add_on_content.end(),
|
||||
[current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); }));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IAddOnContentManager::ListAddOnContentByApplicationId(
|
||||
Out<u32> out_count, OutBuffer<BufferAttr_HipcMapAlias> 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<u32> 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<u32>(FileSys::GetAOCID(content_id)));
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current);
|
||||
}
|
||||
|
||||
// TODO(DarkLordZach): Find the correct error code.
|
||||
|
||||
@@ -29,6 +29,10 @@ public:
|
||||
Result CountAddOnContent(Out<u32> out_count, ClientProcessId process_id);
|
||||
Result ListAddOnContent(Out<u32> out_count, OutBuffer<BufferAttr_HipcMapAlias> out_addons,
|
||||
u32 offset, u32 count, ClientProcessId process_id);
|
||||
Result CountAddOnContentByApplicationId(Out<u32> out_count, u64 application_id);
|
||||
Result ListAddOnContentByApplicationId(Out<u32> out_count,
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_addons,
|
||||
u32 offset, u32 count, u64 application_id);
|
||||
Result GetAddOnContentBaseId(Out<u64> out_title_id, ClientProcessId process_id);
|
||||
Result PrepareAddOnContent(s32 addon_index, ClientProcessId process_id);
|
||||
Result GetAddOnContentListChangedEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,6 +50,8 @@ private:
|
||||
OutArray<u8, BufferAttr_HipcMapAlias | BufferAttr_HipcMapTransferAllowsNonSecure> out_image,
|
||||
OutArray<u8, BufferAttr_HipcMapAlias> out_buffer);
|
||||
|
||||
Result SetShimLibraryVersion(u64 shim_library_version, u64 applet_resource_user_id);
|
||||
|
||||
Result TranslateResult(Result in_result);
|
||||
|
||||
std::shared_ptr<AlbumManager> manager = nullptr;
|
||||
|
||||
@@ -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<FileSys::SaveDataFactory> 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<FileSys::SaveDataFactory> 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<FileSys::SaveDataFactory>(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<FileSys::SaveDataFactory> FileSystemController::CreateSaveDataFa
|
||||
LOG_INFO(Service_FS, "Save Path: No overrides found. Using the determined base directory.");
|
||||
return std::make_shared<FileSys::SaveDataFactory>(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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<IFileSystem> out_interface,
|
||||
FileSys::SaveDataSpaceId space_id,
|
||||
FileSys::SaveDataAttribute attribute) {
|
||||
LOG_INFO(Service_FS, "called, space_id={:02X}, program_id={:016X}",
|
||||
static_cast<u8>(space_id), attribute.program_id);
|
||||
LOG_INFO(Service_FS, "called, space_id={:02X}, program_id={:016X}", static_cast<u8>(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<IFileSystem> 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<IFileSystem>(
|
||||
system, std::move(dir), SizeGetter::FromStorageId(fsc, id),
|
||||
save_data_controller->GetFactory(), space_id, attribute);
|
||||
*out_interface =
|
||||
std::make_shared<IFileSystem>(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(InBuffer<BufferAttr_HipcMapAlia
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
std::memcpy(&extra_data, buffer.data(), sizeof(FileSys::SaveDataExtraData));
|
||||
|
||||
R_RETURN(save_data_controller->WriteSaveDataExtraData(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(OutBuffer<BufferAttr_HipcMapAlia
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
R_TRY(save_data_controller->ReadSaveDataExtraData(&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<ISaveDataTransferManage
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result FSP_SRV::OpenSaveDataTransferManagerVersion2(OutInterface<ISaveDataTransferManager> out_interface) {
|
||||
Result FSP_SRV::OpenSaveDataTransferManagerVersion2(
|
||||
OutInterface<ISaveDataTransferManager> out_interface) {
|
||||
LOG_DEBUG(Service_FS, "called");
|
||||
|
||||
*out_interface = std::make_shared<ISaveDataTransferManager>(system);
|
||||
@@ -690,18 +691,28 @@ Result FSP_SRV::DeleteSaveDataFileSystem(u64 save_data_id) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result FSP_SRV::OpenGameCardStorage(OutInterface<IStorage> out_interface, u32 handle, u32 partition_id) {
|
||||
Result FSP_SRV::OpenGameCardStorage(OutInterface<IStorage> 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<IFileSystem> out_interface, u32 handle, u32 partition_id) {
|
||||
Result FSP_SRV::OpenGameCardFileSystem(OutInterface<IFileSystem> 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<bool> out_is_supported) {
|
||||
LOG_WARNING(Service_FS, "(STUBBED) called");
|
||||
|
||||
*out_is_supported = true;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace Service::FileSystem
|
||||
|
||||
@@ -116,12 +116,15 @@ private:
|
||||
Result OpenSdCardDetectionEventNotifier(OutInterface<IEventNotifier> out_interface);
|
||||
Result OpenGameCardDetectionEventNotifier(OutInterface<IEventNotifier> out_interface);
|
||||
Result OpenSaveDataTransferManager(OutInterface<ISaveDataTransferManager> out_interface);
|
||||
Result OpenSaveDataTransferManagerVersion2(OutInterface<ISaveDataTransferManager> out_interface);
|
||||
Result OpenSaveDataTransferManagerVersion2(
|
||||
OutInterface<ISaveDataTransferManager> out_interface);
|
||||
Result OpenBisWiper(OutInterface<IWiper> out_interface);
|
||||
Result OpenBisStorage(OutInterface<IStorage> out_interface, u32 partition_id);
|
||||
Result DeleteSaveDataFileSystem(u64 save_data_id);
|
||||
Result OpenGameCardStorage(OutInterface<IStorage> out_interface, u32 handle, u32 partition_id);
|
||||
Result OpenGameCardFileSystem(OutInterface<IFileSystem> out_interface, u32 handle, u32 partition_id);
|
||||
Result OpenGameCardFileSystem(OutInterface<IFileSystem> out_interface, u32 handle,
|
||||
u32 partition_id);
|
||||
Result IsExFatSupported(Out<bool> out_is_supported);
|
||||
|
||||
FileSystemController& fsc;
|
||||
const FileSys::ContentProvider& content_provider;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#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<IScanRequest> {
|
||||
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<u8> 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<u32*>(base + 0x0) = 2u;
|
||||
*reinterpret_cast<u32*>(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<u32>(static_cast<u32>(adapters.size()));
|
||||
}
|
||||
|
||||
void IGeneralService::RemoveNetworkProfile(HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_NIFM, "(STUBBED) called");
|
||||
|
||||
@@ -691,6 +727,31 @@ void IGeneralService::GetCurrentIpConfigInfo(HLERequestContext& ctx) {
|
||||
rb.PushRaw<IpConfigInfo>(ip_config_info);
|
||||
}
|
||||
|
||||
void IGeneralService::EnumerateNetworkProfiles(HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_NIFM, "(STUBBED) called");
|
||||
|
||||
// Return 0 profiles for now (stub)
|
||||
ctx.WriteBuffer(std::span<u8>{});
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 3};
|
||||
rb.Push(ResultSuccess);
|
||||
rb.Push<u32>(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<u8>(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"},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<u32> out_state) {
|
||||
LOG_INFO(Service_NPNS, "called");
|
||||
*out_state = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetLastNotifiedTime(Out<s64> out_last_notified_time) {
|
||||
LOG_INFO(Service_NPNS, "called");
|
||||
*out_last_notified_time = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetRequestChangeStateCancelEvent(OutCopyHandle<Kernel::KReadableEvent> 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<INpnsUser> {
|
||||
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<BufferAttr_HipcMapAlias> name_buffer) {
|
||||
const std::string name(reinterpret_cast<const char*>(name_buffer.data()),
|
||||
name_buffer.size());
|
||||
LOG_DEBUG(Service_NPNS, "called, name={}", name);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetReceiveEvent(OutCopyHandle<Kernel::KReadableEvent> out_event) {
|
||||
LOG_DEBUG(Service_NPNS, "called");
|
||||
|
||||
*out_event = &get_receive_event->GetReadableEvent();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetState(Out<u32> 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<INotificationReceiver> {
|
||||
@@ -198,8 +256,7 @@ public:
|
||||
|
||||
class IFuture : public ServiceFramework<IFuture> {
|
||||
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)
|
||||
|
||||
@@ -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<BufferAttr_HipcMapAlias> out_buffer, Out<u32> 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<ApplicationLanguage> 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<u64> 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<s32> 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<ApplicationRecord, BufferAttr_HipcMapAlias> out_records, Out<s32> 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<ApplicationRecord> 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<size_t>(std::max(0, offset));
|
||||
for (size_t j = start; j < records.size() && i < limit; ++j) {
|
||||
out_records[i++] = records[j];
|
||||
}
|
||||
|
||||
*out_count = static_cast<s32>(i);
|
||||
@@ -463,7 +502,7 @@ Result IApplicationManagerInterface::ListApplicationRecord(
|
||||
|
||||
Result IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent(
|
||||
OutCopyHandle<Kernel::KReadableEvent> 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<Kernel::KReadableEvent> 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<bool> 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<ApplicationViewV20, BufferAttr_HipcMapAlias> out_application_views,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> 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<ApplicationView, BufferAttr_HipcMapAlias> out_application_views,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> 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<ApplicationViewWithPromotionInfo, BufferAttr_HipcMapAlias> out_application_views,
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, Out<u32> out_count,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> 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<u32>(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<Kernel::KReadableEvent> 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<s64> 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<Kernel::KReadableEvent> 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<u64> out_result) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IApplicationManagerInterface::Cmd4042(Out<u64> out_result) {
|
||||
LOG_DEBUG(Service_NS, "(STUBBED) called [20.0.0+]");
|
||||
*out_result = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IApplicationManagerInterface::CountApplicationContentMeta(Out<u32> 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<u32, BufferAttr_HipcMapAlias> out_addons, Out<u32> 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<u32> 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<u32>(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<size_t>(std::max<u32>(0, offset));
|
||||
for (size_t j = start; j < aocs.size() && i < limit; ++j) {
|
||||
out_addons[i++] = aocs[j];
|
||||
}
|
||||
|
||||
*out_count = static_cast<u32>(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
|
||||
|
||||
@@ -24,17 +24,23 @@ public:
|
||||
u32 supported_languages);
|
||||
Result ConvertApplicationLanguageToLanguageCode(Out<u64> out_language_code,
|
||||
ApplicationLanguage application_language);
|
||||
Result GenerateApplicationRecordCount(Out<s32> out_count);
|
||||
Result ListApplicationRecord(OutArray<ApplicationRecord, BufferAttr_HipcMapAlias> out_records,
|
||||
Out<s32> out_count, s32 offset);
|
||||
Result GetApplicationRecordUpdateSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
|
||||
Result GetGameCardMountFailureEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
|
||||
Result IsAnyApplicationEntityInstalled(Out<bool> out_is_any_application_entity_installed);
|
||||
Result GetApplicationView(
|
||||
OutArray<ApplicationViewV20, BufferAttr_HipcMapAlias> out_application_views,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids);
|
||||
Result GetApplicationViewDeprecated(
|
||||
OutArray<ApplicationView, BufferAttr_HipcMapAlias> out_application_views,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids);
|
||||
Result GetApplicationViewWithPromotionInfo(
|
||||
OutArray<ApplicationViewWithPromotionInfo, BufferAttr_HipcMapAlias> out_application_views,
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, Out<u32> out_count,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids);
|
||||
Result RequestDownloadApplicationControlDataInBackground(u64 control_source,
|
||||
u64 application_id);
|
||||
Result GetApplicationRightsOnClient(
|
||||
OutArray<ApplicationRightsOnClient, BufferAttr_HipcMapAlias> out_rights, Out<u32> 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<u64> out_result);
|
||||
Result Cmd4023(Out<u64> out_result);
|
||||
Result Cmd4042(Out<u64> out_result);
|
||||
Result Cmd4053();
|
||||
Result Cmd4088(Out<u64> out_result);
|
||||
// [1.0.0+]
|
||||
Result CountApplicationContentMeta(Out<u32> out_count, u64 application_id);
|
||||
Result ListAvailableAddOnContent(OutArray<u32, BufferAttr_HipcMapAlias> out_addons,
|
||||
Out<u32> out_count, u32 offset, u64 application_id);
|
||||
|
||||
void ListApplicationTitle(HLERequestContext& ctx);
|
||||
|
||||
private:
|
||||
KernelHelpers::ServiceContext service_context;
|
||||
|
||||
@@ -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<u8, 0x2> 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<u8, 0x10> unk_x10; ///< Unknown.
|
||||
u32 unk_x20; ///< Unknown.
|
||||
u16 unk_x24; ///< Unknown.
|
||||
std::array<u8, 0x2> unk_x26; ///< Unknown.
|
||||
std::array<u8, 0x8> unk_x28; ///< Unknown.
|
||||
std::array<u8, 0x10> unk_x30; ///< Unknown.
|
||||
u32 unk_x40; ///< Unknown.
|
||||
u8 unk_x44; ///< Unknown.
|
||||
std::array<u8, 0xb> 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 {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#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<s32> out_count, OutArray<u64, BufferAttr_HipcMapAlias> 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<u64> 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<s32>(count);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IQueryService::QueryApplicationPlayStatisticsForSystem(
|
||||
Out<s32> out_entries, u8 flag,
|
||||
OutArray<ApplicationPlayStatistics, BufferAttr_HipcMapAlias> out_stats,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> 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<s32> out_entries, u8 flag, Common::UUID user_id,
|
||||
OutArray<ApplicationPlayStatistics, BufferAttr_HipcMapAlias> out_stats,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids) {
|
||||
LOG_INFO(Service_NS, "called, user_id={}", user_id.FormattedString());
|
||||
return QueryApplicationPlayStatisticsForSystem(out_entries, flag, out_stats, application_ids);
|
||||
}
|
||||
|
||||
} // namespace Service::NS
|
||||
|
||||
@@ -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<IQueryService> {
|
||||
public:
|
||||
explicit IQueryService(Core::System& system_);
|
||||
@@ -31,6 +43,20 @@ public:
|
||||
private:
|
||||
Result QueryPlayStatisticsByApplicationIdAndUserAccountId(
|
||||
Out<PlayStatistics> out_play_statistics, bool unknown, u64 application_id, Uid account_id);
|
||||
|
||||
Result QueryRecentlyPlayedApplication(Out<s32> out_count,
|
||||
OutArray<u64, BufferAttr_HipcMapAlias> out_applications,
|
||||
Uid user_id);
|
||||
|
||||
Result QueryApplicationPlayStatisticsForSystem(
|
||||
Out<s32> out_entries, u8 flag,
|
||||
OutArray<ApplicationPlayStatistics, BufferAttr_HipcMapAlias> out_stats,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids);
|
||||
|
||||
Result QueryApplicationPlayStatisticsByUserAccountIdForSystem(
|
||||
Out<s32> out_entries, u8 flag, Common::UUID user_id,
|
||||
OutArray<ApplicationPlayStatistics, BufferAttr_HipcMapAlias> out_stats,
|
||||
InArray<u64, BufferAttr_HipcMapAlias> application_ids);
|
||||
};
|
||||
|
||||
} // namespace Service::NS
|
||||
|
||||
@@ -1,11 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Suppress warnings from stb headers - use compiler-specific pragmas
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4244) // conversion, possible loss of data
|
||||
#pragma warning(disable : 4456) // declaration hides previous local declaration
|
||||
#pragma warning(disable : 4457) // declaration hides function parameter
|
||||
#pragma warning(disable : 4701) // potentially uninitialized local variable
|
||||
#pragma warning(disable : 4703) // potentially uninitialized local pointer variable
|
||||
#elif defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-function"
|
||||
#pragma clang diagnostic ignored "-Wimplicit-int-conversion"
|
||||
#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion"
|
||||
#pragma clang diagnostic ignored "-Wimplicit-fallthrough"
|
||||
#pragma clang diagnostic ignored "-Wfloat-conversion"
|
||||
#pragma clang diagnostic ignored "-Wconversion"
|
||||
#pragma clang diagnostic ignored "-Wsign-conversion"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#pragma GCC diagnostic ignored "-Wconversion"
|
||||
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
|
||||
#endif
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||
#define STB_IMAGE_RESIZE_STATIC
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define STB_IMAGE_WRITE_STATIC
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_resize.h>
|
||||
#include <stb_image_write.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#elif defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#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 +59,113 @@
|
||||
|
||||
namespace Service::NS {
|
||||
|
||||
namespace {
|
||||
|
||||
void JPGToMemory(void* context, void* data, int size) {
|
||||
auto* buffer = static_cast<std::vector<u8>*>(context);
|
||||
const auto* char_data = static_cast<const u8*>(data);
|
||||
buffer->insert(buffer->end(), char_data, char_data + size);
|
||||
}
|
||||
|
||||
void SanitizeJPEGImageSize(std::vector<u8>& 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<int>(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<u8> 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<IAsyncValueForListApplicationTitle> {
|
||||
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<s64>(data_size);
|
||||
}
|
||||
|
||||
void Get(HLERequestContext& ctx) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
std::vector<u8> 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 +176,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 +275,191 @@ Result IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLan
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData2(
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, Out<u64> 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<size_t>(bytes.size()), static_cast<size_t>(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<u8> 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<u32>(nacp_size + available_icon_bytes);
|
||||
|
||||
*out_total_size = (static_cast<u64>(total_available) << 32) | static_cast<u64>(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<u64> 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<Kernel::KTransferMemory>(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<IAsyncValueForListApplicationTitle>(
|
||||
system, data_offset, static_cast<s32>(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<BufferAttr_HipcMapAlias> out_buffer, Out<u32> out_flags_a, Out<u32> out_flags_b,
|
||||
Out<u32> 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<size_t>(bytes.size()), static_cast<size_t>(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<u8> 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<u32>(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
|
||||
|
||||
@@ -16,7 +16,6 @@ public:
|
||||
explicit IReadOnlyApplicationControlDataInterface(Core::System& system_);
|
||||
~IReadOnlyApplicationControlDataInterface() override;
|
||||
|
||||
public:
|
||||
Result GetApplicationControlData(OutBuffer<BufferAttr_HipcMapAlias> out_buffer,
|
||||
Out<u32> out_actual_size,
|
||||
ApplicationControlSource application_control_source,
|
||||
@@ -25,6 +24,16 @@ public:
|
||||
u32 supported_languages);
|
||||
Result ConvertApplicationLanguageToLanguageCode(Out<u64> out_language_code,
|
||||
ApplicationLanguage application_language);
|
||||
Result GetApplicationControlData2(OutBuffer<BufferAttr_HipcMapAlias> out_buffer,
|
||||
Out<u64> out_total_size,
|
||||
ApplicationControlSource application_control_source, u8 flag1,
|
||||
u8 flag2, u64 application_id);
|
||||
void ListApplicationTitle(HLERequestContext& ctx);
|
||||
Result GetApplicationControlData3(OutBuffer<BufferAttr_HipcMapAlias> out_buffer,
|
||||
Out<u32> out_flags_a, Out<u32> out_flags_b,
|
||||
Out<u32> out_actual_size,
|
||||
ApplicationControlSource application_control_source, u8 flag1,
|
||||
u8 flag2, u64 application_id);
|
||||
};
|
||||
|
||||
} // namespace Service::NS
|
||||
|
||||
@@ -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<ApplicationRecord, BufferAttr_HipcMapAlias> out_records, Out<s32> 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
|
||||
|
||||
@@ -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<bool> out_has_application_record, u64 program_id);
|
||||
Result IsDataCorruptedResult(Out<bool> out_is_data_corrupted_result, Result result);
|
||||
Result ListApplicationRecord(OutArray<ApplicationRecord, BufferAttr_HipcMapAlias> out_records,
|
||||
Out<s32> out_count, s32 entry_offset);
|
||||
};
|
||||
|
||||
} // namespace Service::NS
|
||||
|
||||
@@ -42,77 +42,77 @@ IServiceGetterInterface::~IServiceGetterInterface() = default;
|
||||
|
||||
Result IServiceGetterInterface::GetDynamicRightsInterface(
|
||||
Out<SharedPointer<IDynamicRightsInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IDynamicRightsInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetReadOnlyApplicationControlDataInterface(
|
||||
Out<SharedPointer<IReadOnlyApplicationControlDataInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IReadOnlyApplicationControlDataInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetReadOnlyApplicationRecordInterface(
|
||||
Out<SharedPointer<IReadOnlyApplicationRecordInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IReadOnlyApplicationRecordInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetECommerceInterface(
|
||||
Out<SharedPointer<IECommerceInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IECommerceInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetApplicationVersionInterface(
|
||||
Out<SharedPointer<IApplicationVersionInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IApplicationVersionInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetFactoryResetInterface(
|
||||
Out<SharedPointer<IFactoryResetInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IFactoryResetInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetAccountProxyInterface(
|
||||
Out<SharedPointer<IAccountProxyInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IAccountProxyInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetApplicationManagerInterface(
|
||||
Out<SharedPointer<IApplicationManagerInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IApplicationManagerInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetDownloadTaskInterface(
|
||||
Out<SharedPointer<IDownloadTaskInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IDownloadTaskInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetContentManagementInterface(
|
||||
Out<SharedPointer<IContentManagementInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IContentManagementInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IServiceGetterInterface::GetDocumentInterface(
|
||||
Out<SharedPointer<IDocumentInterface>> out_interface) {
|
||||
LOG_DEBUG(Service_NS, "called");
|
||||
LOG_INFO(Service_NS, "called");
|
||||
*out_interface = std::make_shared<IDocumentInterface>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HeapMapper> mapper{};
|
||||
|
||||
@@ -160,7 +160,6 @@ void NVDRV::Initialize(HLERequestContext& ctx) {
|
||||
};
|
||||
|
||||
if (is_initialized) {
|
||||
// No need to initialize again
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ struct Layer {
|
||||
explicit Layer(std::shared_ptr<android::BufferItemConsumer> 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 {
|
||||
|
||||
@@ -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<android::BufferTransformFlags>(item.transform),
|
||||
.crop_rect = item.crop,
|
||||
|
||||
@@ -121,6 +121,13 @@ std::shared_ptr<Layer> 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<android::BufferQueueCore>();
|
||||
|
||||
@@ -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<Layer> FindLayer(s32 consumer_binder_id);
|
||||
|
||||
private:
|
||||
Display* FindDisplay(u64 display_id);
|
||||
std::shared_ptr<Layer> FindLayer(s32 consumer_binder_id);
|
||||
|
||||
public:
|
||||
// TODO: these don't belong here
|
||||
|
||||
@@ -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<boo
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IDaemonController::GetGlobalUploadEnabledForAccount(Out<bool> 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<bool> 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<u8> 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
|
||||
|
||||
@@ -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<bool> out_is_enabled,
|
||||
Common::UUID user_id, u64 application_id);
|
||||
Result GetGlobalUploadEnabledForAccount(Out<bool> out_is_enabled, Common::UUID user_id);
|
||||
Result SetGlobalUploadEnabledForAccount(bool is_enabled, Common::UUID user_id);
|
||||
Result GetGlobalDownloadEnabledForAccount(Out<bool> out_is_enabled, Common::UUID user_id);
|
||||
Result SetGlobalDownloadEnabledForAccount(bool is_enabled, Common::UUID user_id);
|
||||
Result GetAutonomyTaskStatus(Out<u8> out_status, Common::UUID user_id);
|
||||
};
|
||||
|
||||
} // namespace Service::OLSC
|
||||
|
||||
@@ -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<u8> 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<std::array<u8, 0x38>> 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<bool> out_has_secondary_save,
|
||||
Out<std::array<u64, 3>> out_unknown,
|
||||
u64 application_id) {
|
||||
|
||||
@@ -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<u8> out_newness, u64 application_id);
|
||||
Result GetDataInfo(Out<std::array<u8, 0x38>> out_data, u64 application_id);
|
||||
Result GetSecondarySave(Out<bool> out_has_secondary_save, Out<std::array<u64, 3>> out_unknown,
|
||||
u64 application_id);
|
||||
Result Unknown28(); // [20.2.0+]
|
||||
Result Unknown28(); // [20.2.0+]
|
||||
Result Unknown901(); // [20.2.0+]
|
||||
};
|
||||
|
||||
|
||||
@@ -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<SharedPointer<IRemoteStorageController>> out_controller) {
|
||||
LOG_WARNING(Service_OLSC, "(STUBBED) called");
|
||||
*out_controller = std::make_shared<IRemoteStorageController>(system);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ITransferTaskListController::Unknown20() {
|
||||
LOG_WARNING(Service_OLSC, "(STUBBED) called");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ITransferTaskListController::GetCurrentTransferTaskInfo(Out<std::array<u8, 0x30>> out_info,
|
||||
u8 unknown) {
|
||||
LOG_WARNING(Service_OLSC, "(STUBBED) called, unknown={:#x}", unknown);
|
||||
out_info->fill(0);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ITransferTaskListController::FindTransferTaskInfo(Out<std::array<u8, 0x30>> out_info,
|
||||
InBuffer<BufferAttr_HipcAutoSelect> in) {
|
||||
LOG_WARNING(Service_OLSC, "(STUBBED) called, in_size={}", in.size());
|
||||
out_info->fill(0);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace Service::OLSC
|
||||
|
||||
@@ -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<ITransferTaskListController> {
|
||||
public:
|
||||
@@ -15,6 +17,11 @@ public:
|
||||
|
||||
private:
|
||||
Result GetNativeHandleHolder(Out<SharedPointer<INativeHandleHolder>> out_holder);
|
||||
Result GetRemoteStorageController(Out<SharedPointer<IRemoteStorageController>> out_controller);
|
||||
Result Unknown20();
|
||||
Result GetCurrentTransferTaskInfo(Out<std::array<u8, 0x30>> out_info, u8 unknown);
|
||||
Result FindTransferTaskInfo(Out<std::array<u8, 0x30>> out_info,
|
||||
InBuffer<BufferAttr_HipcAutoSelect> in);
|
||||
};
|
||||
|
||||
} // namespace Service::OLSC
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Kernel::KReadableEvent> 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<Kernel::KReadableEvent> out_event) {
|
||||
LOG_INFO(Service_PSC, "called");
|
||||
*out_event = &receive_event->GetReadableEvent();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IReceiver::Receive(Out<OverlayNotification> out_notification, Out<MessageFlags> out_flags) {
|
||||
u64 tick;
|
||||
return ReceiveWithTick(out_notification, out_flags, Out<u64>(&tick));
|
||||
}
|
||||
|
||||
Result IReceiver::ReceiveWithTick(Out<OverlayNotification> out_notification,
|
||||
Out<MessageFlags> out_flags, Out<u64> 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
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#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<Kernel::KReadableEvent> out_event);
|
||||
Result Receive(Out<OverlayNotification> out_notification, Out<MessageFlags> out_flags);
|
||||
Result ReceiveWithTick(Out<OverlayNotification> out_notification, Out<MessageFlags> out_flags,
|
||||
Out<u64> out_tick);
|
||||
|
||||
KernelHelpers::ServiceContext service_context;
|
||||
Kernel::KEvent* event;
|
||||
Kernel::KEvent* receive_event;
|
||||
|
||||
std::map<std::string, std::vector<std::pair<OverlayNotification, MessageFlags>>>
|
||||
message_sources;
|
||||
};
|
||||
|
||||
} // namespace Service::PSC
|
||||
|
||||
@@ -431,6 +431,15 @@ static_assert(sizeof(FirmwareVersionFormat) == 0x100, "FirmwareVersionFormat is
|
||||
static_assert(std::is_trivial_v<FirmwareVersionFormat>,
|
||||
"FirmwareVersionFormat type must be trivially copyable.");
|
||||
|
||||
/// This is nn::settings::system::RebootlessSystemUpdateVersion
|
||||
struct RebootlessSystemUpdateVersion {
|
||||
u32 version;
|
||||
std::array<char, 0x18> 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;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
#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<RebootlessSystemUpdateVersion> 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<Service::PSC::Time::LocationName> out_name) {
|
||||
LOG_INFO(Service_SET, "called");
|
||||
|
||||
@@ -92,6 +92,7 @@ public:
|
||||
Result SetSpeakerAutoMuteFlag(bool force_mute_on_headphone_removed);
|
||||
Result GetQuestFlag(Out<QuestFlag> out_quest_flag);
|
||||
Result SetQuestFlag(QuestFlag quest_flag);
|
||||
Result GetRebootlessSystemUpdateVersion(Out<RebootlessSystemUpdateVersion> out_version);
|
||||
Result GetDeviceTimeZoneLocationName(Out<Service::PSC::Time::LocationName> out_name);
|
||||
Result SetDeviceTimeZoneLocationName(const Service::PSC::Time::LocationName& name);
|
||||
Result SetRegionCode(SystemRegionCode region_code);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<u64> out_layer_id, u32 flags, u64 display_id,
|
||||
AppletResourceUserId aruid) {
|
||||
LOG_DEBUG(Service_VI, "called. flags={}, display={}, aruid={}", flags, display_id, aruid.pid);
|
||||
|
||||
@@ -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<u64> out_layer_id, u32 flags, u64 display_id,
|
||||
|
||||
Reference in New Issue
Block a user