diff options
Diffstat (limited to 'src/yuzu')
-rw-r--r-- | src/yuzu/CMakeLists.txt | 8 | ||||
-rw-r--r-- | src/yuzu/configuration/config.cpp | 11 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_general.cpp | 2 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_general.ui | 29 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_system.cpp | 260 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_system.h | 33 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_system.ui | 252 | ||||
-rw-r--r-- | src/yuzu/debugger/wait_tree.cpp | 4 | ||||
-rw-r--r-- | src/yuzu/main.cpp | 168 | ||||
-rw-r--r-- | src/yuzu/main.h | 7 | ||||
-rw-r--r-- | src/yuzu/main.ui | 27 |
11 files changed, 670 insertions, 131 deletions
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index b901c29d2..9379d9110 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -82,10 +82,10 @@ set(UIS ) file(GLOB COMPAT_LIST - ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc - ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) -file(GLOB_RECURSE ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*) -file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) + ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc + ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) +file(GLOB_RECURSE ICONS ${PROJECT_SOURCE_DIR}/dist/icons/*) +file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*) qt5_wrap_ui(UI_HDRS ${UIS}) diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 71c6ebb41..1fe9a7edd 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -4,6 +4,7 @@ #include <QSettings> #include "common/file_util.h" +#include "core/hle/service/acc/profile_manager.h" #include "input_common/main.h" #include "yuzu/configuration/config.h" #include "yuzu/ui_settings.h" @@ -122,7 +123,11 @@ void Config::ReadValues() { qt_config->beginGroup("System"); Settings::values.use_docked_mode = qt_config->value("use_docked_mode", false).toBool(); - Settings::values.username = qt_config->value("username", "yuzu").toString().toStdString(); + Settings::values.enable_nfc = qt_config->value("enable_nfc", true).toBool(); + + Settings::values.current_user = std::clamp<int>(qt_config->value("current_user", 0).toInt(), 0, + Service::Account::MAX_USERS - 1); + Settings::values.language_index = qt_config->value("language_index", 1).toInt(); qt_config->endGroup(); @@ -258,7 +263,9 @@ void Config::SaveValues() { qt_config->beginGroup("System"); qt_config->setValue("use_docked_mode", Settings::values.use_docked_mode); - qt_config->setValue("username", QString::fromStdString(Settings::values.username)); + qt_config->setValue("enable_nfc", Settings::values.enable_nfc); + qt_config->setValue("current_user", Settings::values.current_user); + qt_config->setValue("language_index", Settings::values.language_index); qt_config->endGroup(); diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index f5db9e55b..537d6e576 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp @@ -31,6 +31,7 @@ void ConfigureGeneral::setConfiguration() { ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); ui->use_cpu_jit->setChecked(Settings::values.use_cpu_jit); ui->use_docked_mode->setChecked(Settings::values.use_docked_mode); + ui->enable_nfc->setChecked(Settings::values.enable_nfc); } void ConfigureGeneral::PopulateHotkeyList(const HotkeyRegistry& registry) { @@ -45,4 +46,5 @@ void ConfigureGeneral::applyConfiguration() { Settings::values.use_cpu_jit = ui->use_cpu_jit->isChecked(); Settings::values.use_docked_mode = ui->use_docked_mode->isChecked(); + Settings::values.enable_nfc = ui->enable_nfc->isChecked(); } diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui index 1775c4d40..b82fffde8 100644 --- a/src/yuzu/configuration/configure_general.ui +++ b/src/yuzu/configuration/configure_general.ui @@ -68,19 +68,26 @@ <property name="title"> <string>Emulation</string> </property> - <layout class="QHBoxLayout" name="EmulationHorizontalLayout"> + <layout class="QHBoxLayout" name="EmulationHorizontalLayout"> + <item> + <layout class="QVBoxLayout" name="EmulationVerticalLayout"> + <item> + <widget class="QCheckBox" name="use_docked_mode"> + <property name="text"> + <string>Enable docked mode</string> + </property> + </widget> + </item> <item> - <layout class="QVBoxLayout" name="EmulationVerticalLayout"> - <item> - <widget class="QCheckBox" name="use_docked_mode"> - <property name="text"> - <string>Enable docked mode</string> - </property> - </widget> - </item> - </layout> + <widget class="QCheckBox" name="enable_nfc"> + <property name="text"> + <string>Enable NFC</string> + </property> + </widget> </item> - </layout> + </layout> + </item> + </layout> </widget> </item> <item> diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp index e9ed9c38f..83cc49dfc 100644 --- a/src/yuzu/configuration/configure_system.cpp +++ b/src/yuzu/configuration/configure_system.cpp @@ -2,13 +2,30 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <algorithm> +#include <QFileDialog> +#include <QGraphicsItem> +#include <QGraphicsScene> +#include <QInputDialog> #include <QMessageBox> +#include <QStandardItemModel> +#include <QTreeView> +#include <QVBoxLayout> +#include "common/common_paths.h" +#include "common/logging/backend.h" +#include "common/string_util.h" #include "core/core.h" +#include "core/hle/service/acc/profile_manager.h" #include "core/settings.h" #include "ui_configure_system.h" #include "yuzu/configuration/configure_system.h" #include "yuzu/main.h" +static std::string GetImagePath(Service::Account::UUID uuid) { + return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg"; +} + static const std::array<int, 12> days_in_month = {{ 31, 29, @@ -24,7 +41,20 @@ static const std::array<int, 12> days_in_month = {{ 31, }}; -ConfigureSystem::ConfigureSystem(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureSystem) { +// Same backup JPEG used by acc IProfile::GetImage if no jpeg found +static constexpr std::array<u8, 107> backup_jpeg{ + 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, + 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09, 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, + 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, + 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff, 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x11, 0x00, 0xff, 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xff, 0xda, 0x00, 0x08, + 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xd2, 0xcf, 0x20, 0xff, 0xd9, +}; + +ConfigureSystem::ConfigureSystem(QWidget* parent) + : QWidget(parent), ui(new Ui::ConfigureSystem), + profile_manager(std::make_unique<Service::Account::ProfileManager>()) { ui->setupUi(this); connect(ui->combo_birthmonth, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, @@ -32,6 +62,45 @@ ConfigureSystem::ConfigureSystem(QWidget* parent) : QWidget(parent), ui(new Ui:: connect(ui->button_regenerate_console_id, &QPushButton::clicked, this, &ConfigureSystem::refreshConsoleID); + layout = new QVBoxLayout; + tree_view = new QTreeView; + item_model = new QStandardItemModel(tree_view); + tree_view->setModel(item_model); + + tree_view->setAlternatingRowColors(true); + tree_view->setSelectionMode(QHeaderView::SingleSelection); + tree_view->setSelectionBehavior(QHeaderView::SelectRows); + tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setSortingEnabled(true); + tree_view->setEditTriggers(QHeaderView::NoEditTriggers); + tree_view->setUniformRowHeights(true); + tree_view->setIconSize({64, 64}); + tree_view->setContextMenuPolicy(Qt::NoContextMenu); + + item_model->insertColumns(0, 1); + item_model->setHeaderData(0, Qt::Horizontal, "Users"); + + // We must register all custom types with the Qt Automoc system so that we are able to use it + // with signals/slots. In this case, QList falls under the umbrells of custom types. + qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); + + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(tree_view); + + ui->scrollArea->setLayout(layout); + + connect(tree_view, &QTreeView::clicked, this, &ConfigureSystem::SelectUser); + + connect(ui->pm_add, &QPushButton::pressed, this, &ConfigureSystem::AddUser); + connect(ui->pm_rename, &QPushButton::pressed, this, &ConfigureSystem::RenameUser); + connect(ui->pm_remove, &QPushButton::pressed, this, &ConfigureSystem::DeleteUser); + connect(ui->pm_set_image, &QPushButton::pressed, this, &ConfigureSystem::SetUserImage); + + scene = new QGraphicsScene; + ui->current_user_icon->setScene(scene); + this->setConfiguration(); } @@ -39,16 +108,74 @@ ConfigureSystem::~ConfigureSystem() = default; void ConfigureSystem::setConfiguration() { enabled = !Core::System::GetInstance().IsPoweredOn(); - ui->edit_username->setText(QString::fromStdString(Settings::values.username)); + ui->combo_language->setCurrentIndex(Settings::values.language_index); + + item_model->removeRows(0, item_model->rowCount()); + list_items.clear(); + + PopulateUserList(); + UpdateCurrentUser(); +} + +static QPixmap GetIcon(Service::Account::UUID uuid) { + const auto icon_url = QString::fromStdString(GetImagePath(uuid)); + QPixmap icon{icon_url}; + + if (!icon) { + icon.fill(Qt::black); + icon.loadFromData(backup_jpeg.data(), backup_jpeg.size()); + } + + return icon; +} + +void ConfigureSystem::PopulateUserList() { + const auto& profiles = profile_manager->GetAllUsers(); + for (const auto& user : profiles) { + Service::Account::ProfileBase profile; + if (!profile_manager->GetProfileBase(user, profile)) + continue; + + const auto username = Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(profile.username.data()), profile.username.size()); + + list_items.push_back(QList<QStandardItem*>{new QStandardItem{ + GetIcon(user).scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + QString::fromStdString(username + '\n' + user.FormatSwitch())}}); + } + + for (const auto& item : list_items) + item_model->appendRow(item); +} + +void ConfigureSystem::UpdateCurrentUser() { + ui->pm_add->setEnabled(profile_manager->GetUserCount() < Service::Account::MAX_USERS); + + const auto& current_user = profile_manager->GetUser(Settings::values.current_user); + ASSERT(current_user != boost::none); + const auto username = GetAccountUsername(*current_user); + + scene->clear(); + scene->addPixmap( + GetIcon(*current_user).scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + ui->current_user_username->setText(QString::fromStdString(username)); } void ConfigureSystem::ReadSystemSettings() {} +std::string ConfigureSystem::GetAccountUsername(Service::Account::UUID uuid) const { + Service::Account::ProfileBase profile; + if (!profile_manager->GetProfileBase(uuid, profile)) + return ""; + return Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(profile.username.data()), profile.username.size()); +} + void ConfigureSystem::applyConfiguration() { if (!enabled) return; - Settings::values.username = ui->edit_username->text().toStdString(); + Settings::values.language_index = ui->combo_language->currentIndex(); Settings::Apply(); } @@ -92,3 +219,130 @@ void ConfigureSystem::refreshConsoleID() { ui->label_console_id->setText( tr("Console ID: 0x%1").arg(QString::number(console_id, 16).toUpper())); } + +void ConfigureSystem::SelectUser(const QModelIndex& index) { + Settings::values.current_user = + std::clamp<std::size_t>(index.row(), 0, profile_manager->GetUserCount() - 1); + + UpdateCurrentUser(); + + ui->pm_remove->setEnabled(profile_manager->GetUserCount() >= 2); + ui->pm_rename->setEnabled(true); + ui->pm_set_image->setEnabled(true); +} + +void ConfigureSystem::AddUser() { + Service::Account::UUID uuid; + uuid.Generate(); + + bool ok = false; + const auto username = + QInputDialog::getText(this, tr("Enter Username"), tr("Enter a username for the new user:"), + QLineEdit::Normal, QString(), &ok); + if (!ok) + return; + + profile_manager->CreateNewUser(uuid, username.toStdString()); + + item_model->appendRow(new QStandardItem{ + GetIcon(uuid).scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + QString::fromStdString(username.toStdString() + '\n' + uuid.FormatSwitch())}); +} + +void ConfigureSystem::RenameUser() { + const auto user = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(user); + ASSERT(uuid != boost::none); + const auto username = GetAccountUsername(*uuid); + + Service::Account::ProfileBase profile; + if (!profile_manager->GetProfileBase(*uuid, profile)) + return; + + bool ok = false; + const auto new_username = + QInputDialog::getText(this, tr("Enter Username"), tr("Enter a new username:"), + QLineEdit::Normal, QString::fromStdString(username), &ok); + + if (!ok) + return; + + std::fill(profile.username.begin(), profile.username.end(), '\0'); + const auto username_std = new_username.toStdString(); + if (username_std.size() > profile.username.size()) { + std::copy_n(username_std.begin(), std::min(profile.username.size(), username_std.size()), + profile.username.begin()); + } else { + std::copy(username_std.begin(), username_std.end(), profile.username.begin()); + } + + profile_manager->SetProfileBase(*uuid, profile); + + item_model->setItem( + user, 0, + new QStandardItem{ + GetIcon(*uuid).scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + tr("%1\n%2", "%1 is the profile username, %2 is the formatted UUID (e.g. " + "00112233-4455-6677-8899-AABBCCDDEEFF))") + .arg(QString::fromStdString(username_std), + QString::fromStdString(uuid->FormatSwitch()))}); + UpdateCurrentUser(); +} + +void ConfigureSystem::DeleteUser() { + const auto index = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(index); + ASSERT(uuid != boost::none); + const auto username = GetAccountUsername(*uuid); + + const auto confirm = + QMessageBox::question(this, tr("Confirm Delete"), + tr("You are about to delete user with name %1. Are you sure?") + .arg(QString::fromStdString(username))); + + if (confirm == QMessageBox::No) + return; + + if (Settings::values.current_user == tree_view->currentIndex().row()) + Settings::values.current_user = 0; + UpdateCurrentUser(); + + if (!profile_manager->RemoveUser(*uuid)) + return; + + item_model->removeRows(tree_view->currentIndex().row(), 1); + tree_view->clearSelection(); + + ui->pm_remove->setEnabled(false); + ui->pm_rename->setEnabled(false); +} + +void ConfigureSystem::SetUserImage() { + const auto index = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(index); + ASSERT(uuid != boost::none); + const auto username = GetAccountUsername(*uuid); + + const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(), + "JPEG Images (*.jpg *.jpeg)"); + + if (file.isEmpty()) + return; + + FileUtil::Delete(GetImagePath(*uuid)); + + const auto raw_path = + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/8000000000000010"; + if (FileUtil::Exists(raw_path) && !FileUtil::IsDirectory(raw_path)) + FileUtil::Delete(raw_path); + + FileUtil::CreateFullPath(GetImagePath(*uuid)); + FileUtil::Copy(file.toStdString(), GetImagePath(*uuid)); + + item_model->setItem( + index, 0, + new QStandardItem{ + GetIcon(*uuid).scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + QString::fromStdString(username + '\n' + uuid->FormatSwitch())}); + UpdateCurrentUser(); +} diff --git a/src/yuzu/configuration/configure_system.h b/src/yuzu/configuration/configure_system.h index f13de17d4..b73e0719c 100644 --- a/src/yuzu/configuration/configure_system.h +++ b/src/yuzu/configuration/configure_system.h @@ -5,8 +5,21 @@ #pragma once #include <memory> + +#include <QList> #include <QWidget> +namespace Service::Account { +class ProfileManager; +struct UUID; +} // namespace Service::Account + +class QGraphicsScene; +class QStandardItem; +class QStandardItemModel; +class QTreeView; +class QVBoxLayout; + namespace Ui { class ConfigureSystem; } @@ -21,18 +34,36 @@ public: void applyConfiguration(); void setConfiguration(); + void PopulateUserList(); + void UpdateCurrentUser(); + public slots: void updateBirthdayComboBox(int birthmonth_index); void refreshConsoleID(); + void SelectUser(const QModelIndex& index); + void AddUser(); + void RenameUser(); + void DeleteUser(); + void SetUserImage(); + private: void ReadSystemSettings(); + std::string GetAccountUsername(Service::Account::UUID uuid) const; + + QVBoxLayout* layout; + QTreeView* tree_view; + QStandardItemModel* item_model; + QGraphicsScene* scene; + + std::vector<QList<QStandardItem*>> list_items; std::unique_ptr<Ui::ConfigureSystem> ui; bool enabled; - std::u16string username; int birthmonth, birthday; int language_index; int sound_index; + + std::unique_ptr<Service::Account::ProfileManager> profile_manager; }; diff --git a/src/yuzu/configuration/configure_system.ui b/src/yuzu/configuration/configure_system.ui index f3f8db038..020b32a37 100644 --- a/src/yuzu/configuration/configure_system.ui +++ b/src/yuzu/configuration/configure_system.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>360</width> - <height>377</height> + <height>483</height> </rect> </property> <property name="windowTitle"> @@ -22,34 +22,28 @@ <string>System Settings</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label_username"> + <item row="1" column="0"> + <widget class="QLabel" name="label_language"> <property name="text"> - <string>Username</string> + <string>Language</string> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="edit_username"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maxLength"> - <number>32</number> + <item row="0" column="0"> + <widget class="QLabel" name="label_birthday"> + <property name="text"> + <string>Birthday</string> </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_birthday"> + <item row="3" column="0"> + <widget class="QLabel" name="label_console_id"> <property name="text"> - <string>Birthday</string> + <string>Console ID:</string> </property> </widget> </item> - <item row="1" column="1"> + <item row="0" column="1"> <layout class="QHBoxLayout" name="horizontalLayout_birthday2"> <item> <widget class="QComboBox" name="combo_birthmonth"> @@ -120,14 +114,7 @@ </item> </layout> </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_language"> - <property name="text"> - <string>Language</string> - </property> - </widget> - </item> - <item row="2" column="1"> + <item row="1" column="1"> <widget class="QComboBox" name="combo_language"> <property name="toolTip"> <string>Note: this can be overridden when region setting is auto-select</string> @@ -187,31 +174,31 @@ <string>Russian (Русский)</string> </property> </item> - <item> - <property name="text"> - <string>Taiwanese</string> - </property> - </item> - <item> - <property name="text"> - <string>British English</string> - </property> - </item> - <item> - <property name="text"> - <string>Canadian French</string> - </property> - </item> - <item> - <property name="text"> - <string>Latin American Spanish</string> - </property> - </item> - <item> - <property name="text"> - <string>Simplified Chinese</string> - </property> - </item> + <item> + <property name="text"> + <string>Taiwanese</string> + </property> + </item> + <item> + <property name="text"> + <string>British English</string> + </property> + </item> + <item> + <property name="text"> + <string>Canadian French</string> + </property> + </item> + <item> + <property name="text"> + <string>Latin American Spanish</string> + </property> + </item> + <item> + <property name="text"> + <string>Simplified Chinese</string> + </property> + </item> <item> <property name="text"> <string>Traditional Chinese (正體中文)</string> @@ -219,14 +206,14 @@ </item> </widget> </item> - <item row="3" column="0"> + <item row="2" column="0"> <widget class="QLabel" name="label_sound"> <property name="text"> <string>Sound output mode</string> </property> </widget> </item> - <item row="3" column="1"> + <item row="2" column="1"> <widget class="QComboBox" name="combo_sound"> <item> <property name="text"> @@ -245,14 +232,7 @@ </item> </widget> </item> - <item row="4" column="0"> - <widget class="QLabel" name="label_console_id"> - <property name="text"> - <string>Console ID:</string> - </property> - </widget> - </item> - <item row="4" column="1"> + <item row="3" column="1"> <widget class="QPushButton" name="button_regenerate_console_id"> <property name="sizePolicy"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> @@ -272,6 +252,143 @@ </widget> </item> <item> + <widget class="QGroupBox" name="gridGroupBox"> + <property name="title"> + <string>Profile Manager</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="sizeConstraint"> + <enum>QLayout::SetNoConstraint</enum> + </property> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Current User</string> + </property> + </widget> + </item> + <item> + <widget class="QGraphicsView" name="current_user_icon"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="interactive"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="current_user_username"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QScrollArea" name="scrollArea"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="widgetResizable"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="pm_set_image"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Set Image</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="pm_add"> + <property name="text"> + <string>Add</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pm_rename"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Rename</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pm_remove"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <widget class="QLabel" name="label_disable_info"> <property name="text"> <string>System settings are available only when game is not running.</string> @@ -281,19 +398,6 @@ </property> </widget> </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> </layout> </item> </layout> diff --git a/src/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp index 7403e9ccd..0c831c9f4 100644 --- a/src/yuzu/debugger/wait_tree.cpp +++ b/src/yuzu/debugger/wait_tree.cpp @@ -9,8 +9,8 @@ #include "core/core.h" #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" -#include "core/hle/kernel/kernel.h" #include "core/hle/kernel/mutex.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/scheduler.h" #include "core/hle/kernel/thread.h" #include "core/hle/kernel/timer.h" @@ -83,7 +83,7 @@ QString WaitTreeText::GetText() const { } WaitTreeMutexInfo::WaitTreeMutexInfo(VAddr mutex_address) : mutex_address(mutex_address) { - auto& handle_table = Core::System::GetInstance().Kernel().HandleTable(); + const auto& handle_table = Core::CurrentProcess()->GetHandleTable(); mutex_value = Memory::Read32(mutex_address); owner_handle = static_cast<Kernel::Handle>(mutex_value & Kernel::Mutex::MutexOwnerMask); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index bef9df00d..47f494841 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -10,6 +10,7 @@ // VFS includes must be before glad as they will conflict with Windows file api, which uses defines. #include "core/file_sys/vfs.h" #include "core/file_sys/vfs_real.h" +#include "core/hle/service/acc/profile_manager.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile becuase of the Windows // defines. @@ -60,6 +61,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "core/hle/kernel/process.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/fsp_ldr.h" +#include "core/hle/service/nfp/nfp.h" +#include "core/hle/service/sm/sm.h" #include "core/loader/loader.h" #include "core/perf_stats.h" #include "core/settings.h" @@ -100,6 +103,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif +constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000; + /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there * is a bitfield "callout_flags" options, used to track if a message has already been shown to the @@ -422,6 +427,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::SDMC); }); connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); + connect(ui.action_Load_Amiibo, &QAction::triggered, this, &GMainWindow::OnLoadAmiibo); // Emulation connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame); @@ -690,6 +696,7 @@ void GMainWindow::ShutdownGame() { ui.action_Stop->setEnabled(false); ui.action_Restart->setEnabled(false); ui.action_Report_Compatibility->setEnabled(false); + ui.action_Load_Amiibo->setEnabled(false); render_window->hide(); game_list->show(); game_list->setFilterFocus(); @@ -751,12 +758,43 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target open_target = "Save Data"; const std::string nand_dir = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); ASSERT(program_id != 0); - // TODO(tech4me): Update this to work with arbitrary user profile - // Refer to core/hle/service/acc/profile_manager.cpp ProfileManager constructor - constexpr u128 user_id = {1, 0}; + + Service::Account::ProfileManager manager{}; + const auto user_ids = manager.GetAllUsers(); + QStringList list; + for (const auto& user_id : user_ids) { + if (user_id == Service::Account::UUID{}) + continue; + Service::Account::ProfileBase base; + if (!manager.GetProfileBase(user_id, base)) + continue; + + list.push_back(QString::fromStdString(Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(base.username.data()), base.username.size()))); + } + + bool ok = false; + const auto index_string = + QInputDialog::getItem(this, tr("Select User"), + tr("Please select the user's save data you would like to open."), + list, Settings::values.current_user, false, &ok); + if (!ok) + return; + + const auto index = list.indexOf(index_string); + ASSERT(index != -1 && index < 8); + + const auto user_id = manager.GetUser(index); + ASSERT(user_id != boost::none); path = nand_dir + FileSys::SaveDataFactory::GetFullPath(FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, - program_id, user_id, 0); + program_id, user_id->uuid, 0); + + if (!FileUtil::Exists(path)) { + FileUtil::CreateFullPath(path); + FileUtil::CreateDir(path); + } + break; } case GameListOpenTarget::ModData: { @@ -823,14 +861,10 @@ static bool RomFSRawCopy(QProgressDialog& dialog, const FileSys::VirtualDir& src } void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path) { - const auto path = fmt::format("{}{:016X}/romfs", - FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); - - const auto failed = [this, &path] { + const auto failed = [this] { QMessageBox::warning(this, tr("RomFS Extraction Failed!"), tr("There was an error copying the RomFS files or the user " "cancelled the operation.")); - vfs->DeleteDirectory(path); }; const auto loader = Loader::GetLoader(vfs->OpenFile(game_path, FileSys::Mode::Read)); @@ -845,10 +879,24 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa return; } - const auto romfs = - loader->IsRomFSUpdatable() - ? FileSys::PatchManager(program_id).PatchRomFS(file, loader->ReadRomFSIVFCOffset()) - : file; + const auto installed = Service::FileSystem::GetUnionContents(); + auto romfs_title_id = SelectRomFSDumpTarget(*installed, program_id); + + if (!romfs_title_id) { + failed(); + return; + } + + const auto path = fmt::format( + "{}{:016X}/romfs", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), *romfs_title_id); + + FileSys::VirtualFile romfs; + + if (*romfs_title_id == program_id) { + romfs = file; + } else { + romfs = installed->GetEntry(*romfs_title_id, FileSys::ContentRecordType::Data)->GetRomFS(); + } const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full); if (extracted == nullptr) { @@ -860,6 +908,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa if (out == nullptr) { failed(); + vfs->DeleteDirectory(path); return; } @@ -870,8 +919,11 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa "files into the new directory while <br>skeleton will only create the directory " "structure."), {"Full", "Skeleton"}, 0, false, &ok); - if (!ok) + if (!ok) { failed(); + vfs->DeleteDirectory(path); + return; + } const auto full = res == "Full"; const auto entry_size = CalculateRomFSEntrySize(extracted, full); @@ -888,6 +940,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa } else { progress.close(); failed(); + vfs->DeleteDirectory(path); } } @@ -1174,6 +1227,7 @@ void GMainWindow::OnStartGame() { ui.action_Report_Compatibility->setEnabled(true); discord_rpc->Update(); + ui.action_Load_Amiibo->setEnabled(true); } void GMainWindow::OnPauseGame() { @@ -1278,6 +1332,27 @@ void GMainWindow::OnConfigure() { } } +void GMainWindow::OnLoadAmiibo() { + const QString extensions{"*.bin"}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), "", file_filter); + if (!filename.isEmpty()) { + Core::System& system{Core::System::GetInstance()}; + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService<Service::NFP::Module::Interface>("nfp:user"); + if (nfc != nullptr) { + auto nfc_file = FileUtil::IOFile(filename.toStdString(), "rb"); + if (!nfc_file.IsOpen()) { + return; + } + std::vector<u8> amiibo_buffer(nfc_file.GetSize()); + nfc_file.ReadBytes(amiibo_buffer.data(), amiibo_buffer.size()); + nfc_file.Close(); + nfc->LoadAmiibo(amiibo_buffer); + } + } +} + void GMainWindow::OnAbout() { AboutDialog aboutDialog(this); aboutDialog.exec(); @@ -1318,15 +1393,17 @@ void GMainWindow::UpdateStatusBar() { void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { QMessageBox::StandardButton answer; QString status_message; - const QString common_message = tr( - "The game you are trying to load requires additional files from your Switch to be dumped " - "before playing.<br/><br/>For more information on dumping these files, please see the " - "following wiki page: <a " - "href='https://yuzu-emu.org/wiki/" - "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System " - "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to quit " - "back to the game list? Continuing emulation may result in crashes, corrupted save " - "data, or other bugs."); + const QString common_message = + tr("The game you are trying to load requires additional files from your Switch to be " + "dumped " + "before playing.<br/><br/>For more information on dumping these files, please see the " + "following wiki page: <a " + "href='https://yuzu-emu.org/wiki/" + "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System " + "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to " + "quit " + "back to the game list? Continuing emulation may result in crashes, corrupted save " + "data, or other bugs."); switch (result) { case Core::System::ResultStatus::ErrorSystemFiles: { QString message = "yuzu was unable to locate a Switch system archive"; @@ -1357,9 +1434,12 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det this, tr("Fatal Error"), tr("yuzu has encountered a fatal error, please see the log for more details. " "For more information on accessing the log, please see the following page: " - "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How to " - "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game list? " - "Continuing emulation may result in crashes, corrupted save data, or other bugs."), + "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How " + "to " + "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game " + "list? " + "Continuing emulation may result in crashes, corrupted save data, or other " + "bugs."), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); status_message = "Fatal Error encountered"; break; @@ -1459,6 +1539,42 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { } } +boost::optional<u64> GMainWindow::SelectRomFSDumpTarget( + const FileSys::RegisteredCacheUnion& installed, u64 program_id) { + const auto dlc_entries = + installed.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); + std::vector<FileSys::RegisteredCacheEntry> dlc_match; + dlc_match.reserve(dlc_entries.size()); + std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match), + [&program_id, &installed](const FileSys::RegisteredCacheEntry& entry) { + return (entry.title_id & DLC_BASE_TITLE_ID_MASK) == program_id && + installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success; + }); + + std::vector<u64> romfs_tids; + romfs_tids.push_back(program_id); + for (const auto& entry : dlc_match) + romfs_tids.push_back(entry.title_id); + + if (romfs_tids.size() > 1) { + QStringList list{"Base"}; + for (std::size_t i = 1; i < romfs_tids.size(); ++i) + list.push_back(QStringLiteral("DLC %1").arg(romfs_tids[i] & 0x7FF)); + + bool ok; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Target"), + tr("Please select which RomFS you would like to dump."), list, 0, false, &ok); + if (!ok) { + return boost::none; + } + + return romfs_tids[list.indexOf(res)]; + } + + return program_id; +} + bool GMainWindow::ConfirmClose() { if (emu_thread == nullptr || !UISettings::values.confirm_before_closing) return true; diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 3663d6aed..7c7c223e1 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -10,6 +10,7 @@ #include <QMainWindow> #include <QTimer> +#include <boost/optional.hpp> #include "common/common_types.h" #include "core/core.h" #include "ui_main.h" @@ -29,8 +30,9 @@ class WaitTreeWidget; enum class GameListOpenTarget; namespace FileSys { +class RegisteredCacheUnion; class VfsFilesystem; -} +} // namespace FileSys namespace Tegra { class DebugContext; @@ -164,6 +166,7 @@ private slots: void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); void OnMenuRecentFile(); void OnConfigure(); + void OnLoadAmiibo(); void OnAbout(); void OnToggleFilterBar(); void OnDisplayTitleBars(bool); @@ -175,6 +178,8 @@ private slots: void OnReinitializeKeys(ReinitializeKeyBehavior behavior); private: + boost::optional<u64> SelectRomFSDumpTarget(const FileSys::RegisteredCacheUnion&, + u64 program_id); void UpdateStatusBar(); Ui::MainWindow ui; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index dffd9c788..48d099591 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -57,8 +57,8 @@ <string>Recent Files</string> </property> </widget> - <addaction name="action_Install_File_NAND" /> - <addaction name="separator"/> + <addaction name="action_Install_File_NAND"/> + <addaction name="separator"/> <addaction name="action_Load_File"/> <addaction name="action_Load_Folder"/> <addaction name="separator"/> @@ -68,6 +68,8 @@ <addaction name="action_Select_NAND_Directory"/> <addaction name="action_Select_SDMC_Directory"/> <addaction name="separator"/> + <addaction name="action_Load_Amiibo"/> + <addaction name="separator"/> <addaction name="action_Exit"/> </widget> <widget class="QMenu" name="menu_Emulation"> @@ -117,11 +119,14 @@ <addaction name="menu_Tools" /> <addaction name="menu_Help"/> </widget> - <action name="action_Install_File_NAND"> - <property name="text"> - <string>Install File to NAND...</string> - </property> - </action> + <action name="action_Install_File_NAND"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Install File to NAND...</string> + </property> + </action> <action name="action_Load_File"> <property name="text"> <string>Load File...</string> @@ -253,6 +258,14 @@ <string>Restart</string> </property> </action> + <action name="action_Load_Amiibo"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Load Amiibo...</string> + </property> + </action> <action name="action_Report_Compatibility"> <property name="enabled"> <bool>false</bool> |