diff options
| -rw-r--r-- | src/core/hle/service/acc/acc.cpp | 54 | ||||
| -rw-r--r-- | src/core/hle/service/acc/profile_manager.cpp | 147 | ||||
| -rw-r--r-- | src/core/hle/service/acc/profile_manager.h | 21 | ||||
| -rw-r--r-- | src/core/hle/service/am/am.cpp | 33 | ||||
| -rw-r--r-- | src/core/settings.h | 2 | ||||
| -rw-r--r-- | src/yuzu/configuration/config.cpp | 9 | ||||
| -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/main.cpp | 40 | ||||
| -rw-r--r-- | src/yuzu_cmd/config.cpp | 9 | 
11 files changed, 742 insertions, 118 deletions
diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp index e61748ca3..cf065c2e0 100644 --- a/src/core/hle/service/acc/acc.cpp +++ b/src/core/hle/service/acc/acc.cpp @@ -2,9 +2,13 @@  // Licensed under GPLv2 or any later version  // Refer to the license.txt file included. +#include <algorithm>  #include <array> +#include "common/common_paths.h"  #include "common/common_types.h" +#include "common/file_util.h"  #include "common/logging/log.h" +#include "common/string_util.h"  #include "common/swap.h"  #include "core/core_timing.h"  #include "core/hle/ipc_helpers.h" @@ -16,6 +20,9 @@  #include "core/hle/service/acc/profile_manager.h"  namespace Service::Account { + +constexpr u32 MAX_JPEG_IMAGE_SIZE = 0x20000; +  // TODO: RE this structure  struct UserData {      INSERT_PADDING_WORDS(1); @@ -27,6 +34,11 @@ struct UserData {  };  static_assert(sizeof(UserData) == 0x80, "UserData structure has incorrect size"); +static std::string GetImagePath(UUID uuid) { +    return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + +           "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg"; +} +  class IProfile final : public ServiceFramework<IProfile> {  public:      explicit IProfile(UUID user_id, ProfileManager& profile_manager) @@ -73,11 +85,11 @@ private:      }      void LoadImage(Kernel::HLERequestContext& ctx) { -        LOG_WARNING(Service_ACC, "(STUBBED) called"); +        LOG_DEBUG(Service_ACC, "called");          // smallest jpeg https://github.com/mathiasbynens/small/blob/master/jpeg.jpg -        // TODO(mailwl): load actual profile image from disk, width 256px, max size 0x20000 -        constexpr u32 jpeg_size = 107; -        static constexpr std::array<u8, jpeg_size> jpeg{ +        // used as a backup should the one on disk not exist +        constexpr u32 backup_jpeg_size = 107; +        static constexpr std::array<u8, backup_jpeg_size> 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, @@ -87,18 +99,42 @@ private:              0xff, 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,              0x00, 0x00, 0x3f, 0x00, 0xd2, 0xcf, 0x20, 0xff, 0xd9,          }; -        ctx.WriteBuffer(jpeg); +          IPC::ResponseBuilder rb{ctx, 3};          rb.Push(RESULT_SUCCESS); -        rb.Push<u32>(jpeg_size); + +        const FileUtil::IOFile image(GetImagePath(user_id), "rb"); + +        if (!image.IsOpen()) { +            LOG_WARNING(Service_ACC, +                        "Failed to load user provided image! Falling back to built-in backup..."); +            ctx.WriteBuffer(backup_jpeg); +            rb.Push<u32>(backup_jpeg_size); +        } else { +            const auto size = std::min<u32>(image.GetSize(), MAX_JPEG_IMAGE_SIZE); +            std::vector<u8> buffer(size); +            image.ReadBytes(buffer.data(), buffer.size()); + +            ctx.WriteBuffer(buffer.data(), buffer.size()); +            rb.Push<u32>(buffer.size()); +        }      }      void GetImageSize(Kernel::HLERequestContext& ctx) { -        LOG_WARNING(Service_ACC, "(STUBBED) called"); -        constexpr u32 jpeg_size = 107; +        LOG_DEBUG(Service_ACC, "called"); +        constexpr u32 backup_jpeg_size = 107;          IPC::ResponseBuilder rb{ctx, 3};          rb.Push(RESULT_SUCCESS); -        rb.Push<u32>(jpeg_size); + +        const FileUtil::IOFile image(GetImagePath(user_id), "rb"); + +        if (!image.IsOpen()) { +            LOG_WARNING(Service_ACC, +                        "Failed to load user provided image! Falling back to built-in backup..."); +            rb.Push<u32>(backup_jpeg_size); +        } else { +            rb.Push<u32>(std::min<u32>(image.GetSize(), MAX_JPEG_IMAGE_SIZE)); +        }      }      const ProfileManager& profile_manager; diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index bcb3475db..06f7d1b15 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -4,32 +4,57 @@  #include <random>  #include <boost/optional.hpp> +#include "common/file_util.h"  #include "core/hle/service/acc/profile_manager.h"  #include "core/settings.h"  namespace Service::Account { + +struct UserRaw { +    UUID uuid; +    UUID uuid2; +    u64 timestamp; +    ProfileUsername username; +    INSERT_PADDING_BYTES(0x80); +}; +static_assert(sizeof(UserRaw) == 0xC8, "UserRaw has incorrect size."); + +struct ProfileDataRaw { +    INSERT_PADDING_BYTES(0x10); +    std::array<UserRaw, MAX_USERS> users; +}; +static_assert(sizeof(ProfileDataRaw) == 0x650, "ProfileDataRaw has incorrect size."); +  // TODO(ogniK): Get actual error codes  constexpr ResultCode ERROR_TOO_MANY_USERS(ErrorModule::Account, -1);  constexpr ResultCode ERROR_USER_ALREADY_EXISTS(ErrorModule::Account, -2);  constexpr ResultCode ERROR_ARGUMENT_IS_NULL(ErrorModule::Account, 20); -const UUID& UUID::Generate() { +constexpr char ACC_SAVE_AVATORS_BASE_PATH[] = "/system/save/8000000000000010/su/avators/"; + +UUID UUID::Generate() {      std::random_device device;      std::mt19937 gen(device());      std::uniform_int_distribution<u64> distribution(1, std::numeric_limits<u64>::max()); -    uuid[0] = distribution(gen); -    uuid[1] = distribution(gen); -    return *this; +    return UUID{distribution(gen), distribution(gen)};  }  ProfileManager::ProfileManager() { -    // TODO(ogniK): Create the default user we have for now until loading/saving users is added -    auto user_uuid = UUID{1, 0}; -    ASSERT(CreateNewUser(user_uuid, Settings::values.username).IsSuccess()); -    OpenUser(user_uuid); +    ParseUserSaveFile(); + +    if (user_count == 0) +        CreateNewUser(UUID::Generate(), "yuzu"); + +    auto current = std::clamp<int>(Settings::values.current_user, 0, MAX_USERS - 1); +    if (UserExistsIndex(current)) +        current = 0; + +    OpenUser(*GetUser(current));  } -ProfileManager::~ProfileManager() = default; +ProfileManager::~ProfileManager() { +    WriteUserSaveFile(); +}  /// After a users creation it needs to be "registered" to the system. AddToProfiles handles the  /// internal management of the users profiles @@ -101,6 +126,12 @@ ResultCode ProfileManager::CreateNewUser(UUID uuid, const std::string& username)      return CreateNewUser(uuid, username_output);  } +boost::optional<UUID> ProfileManager::GetUser(std::size_t index) const { +    if (index >= MAX_USERS) +        return boost::none; +    return profiles[index].user_uuid; +} +  /// Returns a users profile index based on their user id.  boost::optional<std::size_t> ProfileManager::GetUserIndex(const UUID& uuid) const {      if (!uuid) { @@ -164,6 +195,12 @@ bool ProfileManager::UserExists(UUID uuid) const {      return (GetUserIndex(uuid) != boost::none);  } +bool ProfileManager::UserExistsIndex(std::size_t index) const { +    if (index >= MAX_USERS) +        return false; +    return profiles[index].user_uuid.uuid != INVALID_UUID; +} +  /// Opens a specific user  void ProfileManager::OpenUser(UUID uuid) {      auto idx = GetUserIndex(uuid); @@ -239,4 +276,96 @@ bool ProfileManager::CanSystemRegisterUser() const {      // emulate qlaunch. Update this to dynamically change.  } +bool ProfileManager::RemoveUser(UUID uuid) { +    auto index = GetUserIndex(uuid); +    if (index == boost::none) { +        return false; +    } + +    profiles[*index] = ProfileInfo{}; +    std::stable_partition(profiles.begin(), profiles.end(), +                          [](const ProfileInfo& profile) { return profile.user_uuid; }); +    return true; +} + +bool ProfileManager::SetProfileBase(UUID uuid, const ProfileBase& profile_new) { +    auto index = GetUserIndex(uuid); +    if (profile_new.user_uuid == UUID(INVALID_UUID) || index == boost::none) { +        return false; +    } + +    auto& profile = profiles[*index]; +    profile.user_uuid = profile_new.user_uuid; +    profile.username = profile_new.username; +    profile.creation_time = profile_new.timestamp; + +    return true; +} + +void ProfileManager::ParseUserSaveFile() { +    FileUtil::IOFile save(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + +                              ACC_SAVE_AVATORS_BASE_PATH + "profiles.dat", +                          "rb"); + +    if (!save.IsOpen()) { +        LOG_WARNING(Service_ACC, "Failed to load profile data from save data... Generating new " +                                 "user 'yuzu' with random UUID."); +        return; +    } + +    ProfileDataRaw data; +    if (save.ReadBytes(&data, sizeof(ProfileDataRaw)) != sizeof(ProfileDataRaw)) { +        LOG_WARNING(Service_ACC, "profiles.dat is smaller than expected... Generating new user " +                                 "'yuzu' with random UUID."); +        return; +    } + +    for (std::size_t i = 0; i < MAX_USERS; ++i) { +        const auto& user = data.users[i]; + +        if (user.uuid != UUID(INVALID_UUID)) +            AddUser({user.uuid, user.username, user.timestamp, {}, false}); +    } + +    std::stable_partition(profiles.begin(), profiles.end(), +                          [](const ProfileInfo& profile) { return profile.user_uuid; }); +} + +void ProfileManager::WriteUserSaveFile() { +    ProfileDataRaw raw{}; + +    for (std::size_t i = 0; i < MAX_USERS; ++i) { +        raw.users[i].username = profiles[i].username; +        raw.users[i].uuid2 = profiles[i].user_uuid; +        raw.users[i].uuid = profiles[i].user_uuid; +        raw.users[i].timestamp = profiles[i].creation_time; +    } + +    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); + +    const auto path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + +                      ACC_SAVE_AVATORS_BASE_PATH + "profiles.dat"; + +    if (!FileUtil::CreateFullPath(path)) { +        LOG_WARNING(Service_ACC, "Failed to create full path of profiles.dat. Create the directory " +                                 "nand/system/save/8000000000000010/su/avators to mitigate this " +                                 "issue."); +        return; +    } + +    FileUtil::IOFile save(path, "wb"); + +    if (!save.IsOpen()) { +        LOG_WARNING(Service_ACC, "Failed to write save data to file... No changes to user data " +                                 "made in current session will be saved."); +        return; +    } + +    save.Resize(sizeof(ProfileDataRaw)); +    save.WriteBytes(&raw, sizeof(ProfileDataRaw)); +} +  }; // namespace Service::Account diff --git a/src/core/hle/service/acc/profile_manager.h b/src/core/hle/service/acc/profile_manager.h index bffd4cf4d..235208d56 100644 --- a/src/core/hle/service/acc/profile_manager.h +++ b/src/core/hle/service/acc/profile_manager.h @@ -36,7 +36,7 @@ struct UUID {      }      // TODO(ogniK): Properly generate uuids based on RFC-4122 -    const UUID& Generate(); +    static UUID Generate();      // Set the UUID to {0,0} to be considered an invalid user      void Invalidate() { @@ -45,6 +45,15 @@ struct UUID {      std::string Format() const {          return fmt::format("0x{:016X}{:016X}", uuid[1], uuid[0]);      } + +    std::string FormatSwitch() const { +        std::array<u8, 16> s{}; +        std::memcpy(s.data(), uuid.data(), sizeof(u128)); +        return fmt::format("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{" +                           ":02x}{:02x}{:02x}{:02x}{:02x}", +                           s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], +                           s[12], s[13], s[14], s[15]); +    }  };  static_assert(sizeof(UUID) == 16, "UUID is an invalid size!"); @@ -81,12 +90,13 @@ static_assert(sizeof(ProfileBase) == 0x38, "ProfileBase is an invalid size");  /// objects  class ProfileManager {  public: -    ProfileManager(); // TODO(ogniK): Load from system save +    ProfileManager();      ~ProfileManager();      ResultCode AddUser(const ProfileInfo& user);      ResultCode CreateNewUser(UUID uuid, const ProfileUsername& username);      ResultCode CreateNewUser(UUID uuid, const std::string& username); +    boost::optional<UUID> GetUser(std::size_t index) const;      boost::optional<std::size_t> GetUserIndex(const UUID& uuid) const;      boost::optional<std::size_t> GetUserIndex(const ProfileInfo& user) const;      bool GetProfileBase(boost::optional<std::size_t> index, ProfileBase& profile) const; @@ -100,6 +110,7 @@ public:      std::size_t GetUserCount() const;      std::size_t GetOpenUserCount() const;      bool UserExists(UUID uuid) const; +    bool UserExistsIndex(std::size_t index) const;      void OpenUser(UUID uuid);      void CloseUser(UUID uuid);      UserIDArray GetOpenUsers() const; @@ -108,7 +119,13 @@ public:      bool CanSystemRegisterUser() const; +    bool RemoveUser(UUID uuid); +    bool SetProfileBase(UUID uuid, const ProfileBase& profile); +  private: +    void ParseUserSaveFile(); +    void WriteUserSaveFile(); +      std::array<ProfileInfo, MAX_USERS> profiles{};      std::size_t user_count = 0;      boost::optional<std::size_t> AddToProfiles(const ProfileInfo& profile); diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index ecf72ae24..4ed66d817 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -4,11 +4,13 @@  #include <array>  #include <cinttypes> +#include <cstring>  #include <stack>  #include "core/core.h"  #include "core/hle/ipc_helpers.h"  #include "core/hle/kernel/event.h"  #include "core/hle/kernel/process.h" +#include "core/hle/service/acc/profile_manager.h"  #include "core/hle/service/am/am.h"  #include "core/hle/service/am/applet_ae.h"  #include "core/hle/service/am/applet_oe.h" @@ -26,6 +28,16 @@  namespace Service::AM { +constexpr u32 POP_LAUNCH_PARAMETER_MAGIC = 0xC79497CA; + +struct LaunchParameters { +    u32_le magic; +    u32_le is_account_selected; +    u128 current_user; +    INSERT_PADDING_BYTES(0x70); +}; +static_assert(sizeof(LaunchParameters) == 0x88); +  IWindowController::IWindowController() : ServiceFramework("IWindowController") {      // clang-format off      static const FunctionInfo functions[] = { @@ -724,20 +736,23 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx  }  void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) { -    constexpr std::array<u8, 0x88> data{{ -        0xca, 0x97, 0x94, 0xc7, // Magic -        1,    0,    0,    0,    // IsAccountSelected (bool) -        1,    0,    0,    0,    // User Id (word 0) -        0,    0,    0,    0,    // User Id (word 1) -        0,    0,    0,    0,    // User Id (word 2) -        0,    0,    0,    0     // User Id (word 3) -    }}; +    LaunchParameters params{}; -    std::vector<u8> buffer(data.begin(), data.end()); +    params.magic = POP_LAUNCH_PARAMETER_MAGIC; +    params.is_account_selected = 1; + +    Account::ProfileManager profile_manager{}; +    const auto uuid = profile_manager.GetUser(Settings::values.current_user); +    ASSERT(uuid != boost::none); +    params.current_user = uuid->uuid;      IPC::ResponseBuilder rb{ctx, 2, 0, 1};      rb.Push(RESULT_SUCCESS); + +    std::vector<u8> buffer(sizeof(LaunchParameters)); +    std::memcpy(buffer.data(), ¶ms, buffer.size()); +      rb.PushIpcInterface<AM::IStorage>(buffer);      LOG_DEBUG(Service_AM, "called"); diff --git a/src/core/settings.h b/src/core/settings.h index ca80718e2..b5aeff29b 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -114,7 +114,7 @@ struct Values {      // System      bool use_docked_mode;      bool enable_nfc; -    std::string username; +    int current_user;      int language_index;      // Controls diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index d029590ff..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" @@ -123,7 +124,10 @@ void Config::ReadValues() {      qt_config->beginGroup("System");      Settings::values.use_docked_mode = qt_config->value("use_docked_mode", false).toBool();      Settings::values.enable_nfc = qt_config->value("enable_nfc", true).toBool(); -    Settings::values.username = qt_config->value("username", "yuzu").toString().toStdString(); + +    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(); @@ -260,7 +264,8 @@ void Config::SaveValues() {      qt_config->beginGroup("System");      qt_config->setValue("use_docked_mode", Settings::values.use_docked_mode);      qt_config->setValue("enable_nfc", Settings::values.enable_nfc); -    qt_config->setValue("username", QString::fromStdString(Settings::values.username)); +    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_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/main.cpp b/src/yuzu/main.cpp index be9896614..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. @@ -757,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: { diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index 654a15a5c..b456266a6 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -8,6 +8,7 @@  #include "common/file_util.h"  #include "common/logging/log.h"  #include "common/param_package.h" +#include "core/hle/service/acc/profile_manager.h"  #include "core/settings.h"  #include "input_common/main.h"  #include "yuzu_cmd/config.h" @@ -126,10 +127,10 @@ void Config::ReadValues() {      // System      Settings::values.use_docked_mode = sdl2_config->GetBoolean("System", "use_docked_mode", false);      Settings::values.enable_nfc = sdl2_config->GetBoolean("System", "enable_nfc", true); -    Settings::values.username = sdl2_config->Get("System", "username", "yuzu"); -    if (Settings::values.username.empty()) { -        Settings::values.username = "yuzu"; -    } +    const auto size = sdl2_config->GetInteger("System", "users_size", 0); + +    Settings::values.current_user = std::clamp<int>( +        sdl2_config->GetInteger("System", "current_user", 0), 0, Service::Account::MAX_USERS - 1);      // Miscellaneous      Settings::values.log_filter = sdl2_config->Get("Miscellaneous", "log_filter", "*:Trace");  | 
