From b2e602325cf3ed80ac81b7501465ef8eae567007 Mon Sep 17 00:00:00 2001 From: JPikachu Date: Thu, 26 Jun 2025 18:39:28 +0000 Subject: [PATCH] profile_manager: Implement firmware avatar selector (#205) Adds an option to set a user's profile image from the avatars in the firmware. Background color can be changed with a color picker. Also modifies profile image saving to account for this, and as a result images are now saved as JPEG with 100% quality. Any PNG, JPEG, or BMP can now also be used in the image file picker instead of just JPEG. Using ryujinx's implementation and other parts of the yuzu codebase for reference. Credit: Torzu, lui Reviewed-on: http://vub63vv26q6v27xzv2dtcd25xumubshogm67yrpaz2rculqxs7jlfqad.onion/torzu-emu/torzu/pulls/56 Co-authored-by: lui Co-committed-by: lui Co-authored-by: lui Co-authored-by: Maufeat Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/205 Co-authored-by: JPikachu Co-committed-by: JPikachu --- src/common/settings.h | 2 +- src/core/hle/service/acc/acc.cpp | 32 ++ src/core/hle/service/acc/acc.h | 5 + src/core/hle/service/acc/acc_su.cpp | 7 +- src/core/hle/service/acc/profile_manager.cpp | 44 ++- src/core/hle/service/acc/profile_manager.h | 4 + .../service/am/service/display_controller.cpp | 12 +- .../service/am/service/display_controller.h | 5 + .../ns/application_manager_interface.cpp | 9 +- .../ns/application_manager_interface.h | 4 + .../service/ns/dynamic_rights_interface.cpp | 10 + .../hle/service/ns/dynamic_rights_interface.h | 4 + .../service/pctl/parental_control_service.cpp | 10 +- .../service/pctl/parental_control_service.h | 8 +- .../configure_profile_manager.cpp | 335 ++++++++++++++++-- .../configuration/configure_profile_manager.h | 31 +- .../configure_profile_manager.ui | 16 +- 17 files changed, 498 insertions(+), 40 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index 2c22ae323f..aa7d93c156 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -150,7 +150,7 @@ struct Values { Setting net_connect_applet_mode{linkage, AppletMode::HLE, "net_connect_applet_mode", Category::LibraryApplet}; Setting player_select_applet_mode{ - linkage, AppletMode::HLE, "player_select_applet_mode", Category::LibraryApplet}; + linkage, AppletMode::LLE, "player_select_applet_mode", Category::LibraryApplet}; Setting swkbd_applet_mode{linkage, AppletMode::LLE, "swkbd_applet_mode", Category::LibraryApplet}; Setting mii_edit_applet_mode{linkage, AppletMode::LLE, "mii_edit_applet_mode", diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp index 53decd1dad..4562217d5e 100644 --- a/src/core/hle/service/acc/acc.cpp +++ b/src/core/hle/service/acc/acc.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -646,6 +649,7 @@ public: {2, &IManagerForApplication::EnsureIdTokenCacheAsync, "EnsureIdTokenCacheAsync"}, {3, &IManagerForApplication::LoadIdTokenCache, "LoadIdTokenCache"}, {130, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCacheForApplication"}, + {136, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCache"}, // 19.0.0+ {150, nullptr, "CreateAuthorizationRequest"}, {160, &IManagerForApplication::StoreOpenContext, "StoreOpenContext"}, {170, nullptr, "LoadNetworkServiceLicenseKindAsync"}, @@ -986,6 +990,34 @@ void Module::Interface::CompleteUserRegistration(HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void Module::Interface::DeleteUser(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + Common::UUID user_id = rp.PopRaw(); + LOG_INFO(Service_ACC, "called, uuid={}", user_id.FormattedString()); + if (!profile_manager->RemoveUser(user_id)) { + LOG_ERROR(Service_ACC, "Failed to delete user with uuid={}", user_id.RawString()); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(1U); + return; + } + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + +void Module::Interface::SetUserPosition(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + u64 position = rp.Pop(); + Common::UUID user_id = rp.PopRaw(); + + LOG_DEBUG(Service_ACC, "called, position={} user_id={}", position, user_id.FormattedString()); + + profile_manager->SetUserPosition(position, user_id); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void Module::Interface::GetProfileEditor(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; Common::UUID user_id = rp.PopRaw(); diff --git a/src/core/hle/service/acc/acc.h b/src/core/hle/service/acc/acc.h index 81745b0db3..e64a67674f 100644 --- a/src/core/hle/service/acc/acc.h +++ b/src/core/hle/service/acc/acc.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -36,6 +39,8 @@ public: void InitializeApplicationInfoV2(HLERequestContext& ctx); void BeginUserRegistration(HLERequestContext& ctx); void CompleteUserRegistration(HLERequestContext& ctx); + void DeleteUser(HLERequestContext& ctx); + void SetUserPosition(HLERequestContext& ctx); void GetProfileEditor(HLERequestContext& ctx); void ListQualifiedUsers(HLERequestContext& ctx); void ListOpenContextStoredUsers(HLERequestContext& ctx); diff --git a/src/core/hle/service/acc/acc_su.cpp b/src/core/hle/service/acc/acc_su.cpp index 3a737341f8..7d07ea0343 100644 --- a/src/core/hle/service/acc/acc_su.cpp +++ b/src/core/hle/service/acc/acc_su.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -44,8 +47,8 @@ ACC_SU::ACC_SU(std::shared_ptr module_, std::shared_ptr {200, &ACC_SU::BeginUserRegistration, "BeginUserRegistration"}, {201, &ACC_SU::CompleteUserRegistration, "CompleteUserRegistration"}, {202, nullptr, "CancelUserRegistration"}, - {203, nullptr, "DeleteUser"}, - {204, nullptr, "SetUserPosition"}, + {203, &ACC_SU::DeleteUser, "DeleteUser"}, + {204, &ACC_SU::SetUserPosition, "SetUserPosition"}, {205, &ACC_SU::GetProfileEditor, "GetProfileEditor"}, {206, nullptr, "CompleteUserRegistrationForcibly"}, {210, nullptr, "CreateFloatingRegistrationRequest"}, diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index 55f544184b..80a2b0b72b 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -344,6 +347,7 @@ bool ProfileManager::RemoveUser(UUID uuid) { [](const ProfileInfo& profile) { return profile.user_uuid.IsValid(); }); is_save_needed = true; + WriteUserSaveFile(); return true; } @@ -360,6 +364,7 @@ bool ProfileManager::SetProfileBase(UUID uuid, const ProfileBase& profile_new) { profile.creation_time = profile_new.timestamp; is_save_needed = true; + WriteUserSaveFile(); return true; } @@ -370,9 +375,12 @@ bool ProfileManager::SetProfileBaseAndData(Common::UUID uuid, const ProfileBase& if (index.has_value() && SetProfileBase(uuid, profile_new)) { profiles[*index].data = data_new; is_save_needed = true; + WriteUserSaveFile(); return true; + } else { + LOG_ERROR(Service_ACC, "Failed to set profile base and data for user with UUID: {}", + uuid.RawString()); } - return false; } @@ -437,6 +445,14 @@ void ProfileManager::WriteUserSaveFile() { const auto save_path(FS::GetEdenPath(FS::EdenPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH / "profiles.dat"); + if (FS::IsFile(save_path) && !FS::RemoveFile(save_path)) { + LOG_WARNING(Service_ACC, "Could not remove existing profiles.dat"); + return; + } else { + LOG_INFO(Service_ACC, "Writing profiles.dat to {}", + Common::FS::PathToUTF8String(save_path)); + } + if (!FS::CreateParentDirs(save_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 " @@ -455,4 +471,30 @@ void ProfileManager::WriteUserSaveFile() { is_save_needed = false; } +void ProfileManager::SetUserPosition(u64 position, Common::UUID uuid) { + auto idxOpt = GetUserIndex(uuid); + if (!idxOpt) + return; + + size_t oldIdx = *idxOpt; + if (position >= user_count || position == oldIdx) + return; + + ProfileInfo moving = profiles[oldIdx]; + + if (position < oldIdx) { + for (size_t i = oldIdx; i > position; --i) + profiles[i] = profiles[i - 1]; + } else { + for (size_t i = oldIdx; i < position; ++i) + profiles[i] = profiles[i + 1]; + } + + profiles[position] = std::move(moving); + + is_save_needed = true; + WriteUserSaveFile(); +} + + }; // namespace Service::Account diff --git a/src/core/hle/service/acc/profile_manager.h b/src/core/hle/service/acc/profile_manager.h index f94157300f..d64e42715c 100644 --- a/src/core/hle/service/acc/profile_manager.h +++ b/src/core/hle/service/acc/profile_manager.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -89,6 +92,7 @@ public: Common::UUID GetLastOpenedUser() const; UserIDArray GetStoredOpenedUsers() const; void StoreOpenedUsers(); + void SetUserPosition(u64 position, Common::UUID uuid); bool CanSystemRegisterUser() const; diff --git a/src/core/hle/service/am/service/display_controller.cpp b/src/core/hle/service/am/service/display_controller.cpp index ed71f90930..a02f012a92 100644 --- a/src/core/hle/service/am/service/display_controller.cpp +++ b/src/core/hle/service/am/service/display_controller.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -17,7 +20,7 @@ IDisplayController::IDisplayController(Core::System& system_, std::shared_ptr, "GetLastForegroundCaptureImageEx"}, {6, nullptr, "GetLastApplicationCaptureImageEx"}, {7, D<&IDisplayController::GetCallerAppletCaptureImageEx>, "GetCallerAppletCaptureImageEx"}, {8, D<&IDisplayController::TakeScreenShotOfOwnLayer>, "TakeScreenShotOfOwnLayer"}, @@ -48,6 +51,13 @@ IDisplayController::IDisplayController(Core::System& system_, std::shared_ptr out_was_written, OutBuffer out_image_data) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + *out_was_written = true; + R_SUCCEED(); +} + Result IDisplayController::GetCallerAppletCaptureImageEx( Out out_was_written, OutBuffer out_image_data) { LOG_WARNING(Service_AM, "(STUBBED) called"); diff --git a/src/core/hle/service/am/service/display_controller.h b/src/core/hle/service/am/service/display_controller.h index 406fae21a4..6e0cfff2db 100644 --- a/src/core/hle/service/am/service/display_controller.h +++ b/src/core/hle/service/am/service/display_controller.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -16,6 +19,8 @@ public: ~IDisplayController() override; private: + Result GetLastForegroundCaptureImageEx(Out out_was_written, + OutBuffer out_image_data); Result GetCallerAppletCaptureImageEx(Out out_was_written, OutBuffer out_image_data); Result TakeScreenShotOfOwnLayer(bool unknown0, s32 fbshare_layer_index); diff --git a/src/core/hle/service/ns/application_manager_interface.cpp b/src/core/hle/service/ns/application_manager_interface.cpp index 7a91727f97..f1ddba8231 100644 --- a/src/core/hle/service/ns/application_manager_interface.cpp +++ b/src/core/hle/service/ns/application_manager_interface.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -107,7 +110,7 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {210, nullptr, "DeleteUserSystemSaveData"}, {211, nullptr, "DeleteSaveData"}, {220, nullptr, "UnregisterNetworkServiceAccount"}, - {221, nullptr, "UnregisterNetworkServiceAccountWithUserSaveDataDeletion"}, + {221, D<&IApplicationManagerInterface::UnregisterNetworkServiceAccountWithUserSaveDataDeletion>, "UnregisterNetworkServiceAccountWithUserSaveDataDeletion"}, {300, nullptr, "GetApplicationShellEvent"}, {301, nullptr, "PopApplicationShellEventInfo"}, {302, nullptr, "LaunchLibraryApplet"}, @@ -312,6 +315,10 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ IApplicationManagerInterface::~IApplicationManagerInterface() = default; +Result IApplicationManagerInterface::UnregisterNetworkServiceAccountWithUserSaveDataDeletion(Common::UUID user_id) { + LOG_DEBUG(Service_NS, "called, user_id={}", user_id.FormattedString()); + R_SUCCEED(); +} Result IApplicationManagerInterface::GetApplicationControlData( OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, u64 application_id) { diff --git a/src/core/hle/service/ns/application_manager_interface.h b/src/core/hle/service/ns/application_manager_interface.h index f33d269b35..2def50bd5c 100644 --- a/src/core/hle/service/ns/application_manager_interface.h +++ b/src/core/hle/service/ns/application_manager_interface.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -16,6 +19,7 @@ public: explicit IApplicationManagerInterface(Core::System& system_); ~IApplicationManagerInterface() override; + Result UnregisterNetworkServiceAccountWithUserSaveDataDeletion(Common::UUID user_id); Result GetApplicationControlData(OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, diff --git a/src/core/hle/service/ns/dynamic_rights_interface.cpp b/src/core/hle/service/ns/dynamic_rights_interface.cpp index ce81e203f7..e1d4974268 100644 --- a/src/core/hle/service/ns/dynamic_rights_interface.cpp +++ b/src/core/hle/service/ns/dynamic_rights_interface.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -34,6 +37,7 @@ IDynamicRightsInterface::IDynamicRightsInterface(Core::System& system_) {23, nullptr, "GetLimitedApplicationLicenseUpgradableEvent"}, {24, nullptr, "NotifyLimitedApplicationLicenseUpgradableEventForDebug"}, {25, nullptr, "RequestProceedDynamicRightsState"}, + {26, D<&IDynamicRightsInterface::HasAccountRestrictedRightsInRunningApplications>, "HasAccountRestrictedRightsInRunningApplications"} }; // clang-format on @@ -59,4 +63,10 @@ Result IDynamicRightsInterface::VerifyActivatedRightsOwners(u64 rights_handle) { R_SUCCEED(); } +Result IDynamicRightsInterface::HasAccountRestrictedRightsInRunningApplications(Out out_bool) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_bool = 0; + R_SUCCEED(); +} + } // namespace Service::NS diff --git a/src/core/hle/service/ns/dynamic_rights_interface.h b/src/core/hle/service/ns/dynamic_rights_interface.h index 877e009b0f..0f78c4d5bd 100644 --- a/src/core/hle/service/ns/dynamic_rights_interface.h +++ b/src/core/hle/service/ns/dynamic_rights_interface.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -17,6 +20,7 @@ private: Result NotifyApplicationRightsCheckStart(); Result GetRunningApplicationStatus(Out out_status, u64 rights_handle); Result VerifyActivatedRightsOwners(u64 rights_handle); + Result HasAccountRestrictedRightsInRunningApplications(Out out_bool); }; } // namespace Service::NS diff --git a/src/core/hle/service/pctl/parental_control_service.cpp b/src/core/hle/service/pctl/parental_control_service.cpp index 3483ea30d9..1d990e66d7 100644 --- a/src/core/hle/service/pctl/parental_control_service.cpp +++ b/src/core/hle/service/pctl/parental_control_service.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -21,7 +24,7 @@ IParentalControlService::IParentalControlService(Core::System& system_, Capabili {1002, D<&IParentalControlService::ConfirmLaunchApplicationPermission>, "ConfirmLaunchApplicationPermission"}, {1003, D<&IParentalControlService::ConfirmResumeApplicationPermission>, "ConfirmResumeApplicationPermission"}, {1004, D<&IParentalControlService::ConfirmSnsPostPermission>, "ConfirmSnsPostPermission"}, - {1005, nullptr, "ConfirmSystemSettingsPermission"}, + {1005, D<&IParentalControlService::ConfirmSystemSettingsPermission>, "ConfirmSystemSettingsPermission"}, {1006, D<&IParentalControlService::IsRestrictionTemporaryUnlocked>, "IsRestrictionTemporaryUnlocked"}, {1007, nullptr, "RevertRestrictionTemporaryUnlocked"}, {1008, nullptr, "EnterRestrictedSystemSettings"}, @@ -240,6 +243,11 @@ Result IParentalControlService::ConfirmSnsPostPermission() { R_THROW(PCTL::ResultNoFreeCommunication); } +Result IParentalControlService::ConfirmSystemSettingsPermission() { + LOG_WARNING(Service_PCTL, "(STUBBED) called"); + R_SUCCEED(); +} + Result IParentalControlService::IsRestrictionTemporaryUnlocked( Out out_is_temporary_unlocked) { *out_is_temporary_unlocked = false; diff --git a/src/core/hle/service/pctl/parental_control_service.h b/src/core/hle/service/pctl/parental_control_service.h index d58c75f380..1b1884c4de 100644 --- a/src/core/hle/service/pctl/parental_control_service.h +++ b/src/core/hle/service/pctl/parental_control_service.h @@ -1,5 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator +// Project// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -28,6 +31,7 @@ private: Result ConfirmResumeApplicationPermission(InBuffer restriction_bitset, u64 nacp_flag, u64 application_id); Result ConfirmSnsPostPermission(); + Result ConfirmSystemSettingsPermission(); Result IsRestrictionTemporaryUnlocked(Out out_is_temporary_unlocked); Result IsRestrictedSystemSettingsEntered(Out out_is_restricted_system_settings_entered); Result ConfirmStereoVisionPermission(); diff --git a/src/yuzu/configuration/configure_profile_manager.cpp b/src/yuzu/configuration/configure_profile_manager.cpp index 7fb335ee77..f009800d52 100644 --- a/src/yuzu/configuration/configure_profile_manager.cpp +++ b/src/yuzu/configuration/configure_profile_manager.cpp @@ -4,26 +4,36 @@ // SPDX-FileCopyrightText: 2016 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "yuzu/configuration/configure_profile_manager.h" +#include +#include +#include +#include #include #include #include #include #include +#include #include #include #include + #include "common/assert.h" #include "common/fs/path_util.h" #include "common/settings.h" #include "common/string_util.h" +#include "common/swap.h" #include "core/core.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/romfs.h" +#include "core/file_sys/vfs/vfs.h" #include "core/hle/service/acc/profile_manager.h" +#include "core/hle/service/filesystem/filesystem.h" #include "ui_configure_profile_manager.h" #include "yuzu/util/limitable_input_dialog.h" -#include -#include -#include +#include "yuzu/configuration/configure_profile_manager.h" namespace { // Same backup JPEG used by acc IProfile::GetImage if no jpeg found @@ -117,8 +127,12 @@ ConfigureProfileManager::ConfigureProfileManager(Core::System& system_, QWidget* connect(ui->pm_rename, &QPushButton::clicked, this, &ConfigureProfileManager::RenameUser); connect(ui->pm_remove, &QPushButton::clicked, this, &ConfigureProfileManager::ConfirmDeleteUser); - connect(ui->pm_set_image, &QPushButton::clicked, this, &ConfigureProfileManager::SetUserImage); + connect(ui->pm_set_image, &QPushButton::clicked, this, + &ConfigureProfileManager::SelectImageFile); + connect(ui->pm_select_avatar, &QPushButton::clicked, this, + &ConfigureProfileManager::SelectFirmwareAvatar); + avatar_dialog = new ConfigureProfileManagerAvatarDialog(this); confirm_dialog = new ConfigureProfileManagerDeleteDialog(this); scene = new QGraphicsScene; @@ -198,6 +212,7 @@ void ConfigureProfileManager::SelectUser(const QModelIndex& index) { ui->pm_remove->setEnabled(profile_manager.GetUserCount() >= 2); ui->pm_rename->setEnabled(true); ui->pm_set_image->setEnabled(true); + ui->pm_select_avatar->setEnabled(true); } void ConfigureProfileManager::AddUser() { @@ -271,18 +286,11 @@ void ConfigureProfileManager::DeleteUser(const Common::UUID& uuid) { ui->pm_rename->setEnabled(false); } -void ConfigureProfileManager::SetUserImage() { +void ConfigureProfileManager::SetUserImage(const QImage& image) { const auto index = tree_view->currentIndex().row(); const auto uuid = profile_manager.GetUser(index); ASSERT(uuid); - const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(), - tr("JPEG Images (*.jpg *.jpeg)")); - - if (file.isEmpty()) { - return; - } - const auto image_path = GetImagePath(*uuid); if (QFile::exists(image_path) && !QFile::remove(image_path)) { QMessageBox::warning( @@ -308,29 +316,230 @@ void ConfigureProfileManager::SetUserImage() { return; } - if (!QFile::copy(file, image_path)) { - QMessageBox::warning(this, tr("Error copying user image"), - tr("Unable to copy image from %1 to %2").arg(file, image_path)); + if (!image.save(image_path, "JPEG", 100)) { + QMessageBox::warning(this, tr("Error saving user image"), + tr("Unable to save image to file")); return; } - // Profile image must be 256x256 - QImage image(image_path); - if (image.width() != 256 || image.height() != 256) { - image = image.scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - if (!image.save(image_path)) { - QMessageBox::warning(this, tr("Error resizing user image"), - tr("Unable to resize image")); - return; - } - } - const auto username = GetAccountUsername(profile_manager, *uuid); item_model->setItem(index, 0, new QStandardItem{GetIcon(*uuid), FormatUserEntryText(username, *uuid)}); UpdateCurrentUser(); } +void ConfigureProfileManager::SelectImageFile() { + const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(), + tr("Image Formats (*.jpg *.jpeg *.png *.bmp)")); + if (file.isEmpty()) { + return; + } + + // Profile image must be 256x256 + QImage image(file); + if (image.width() != 256 || image.height() != 256) { + image = image.scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } + SetUserImage(image); +} + +void ConfigureProfileManager::SelectFirmwareAvatar() { + if (!avatar_dialog->AreImagesLoaded()) { + if (!LoadAvatarData()) { + return; + } + } + if (avatar_dialog->exec() == QDialog::Accepted) { + SetUserImage(avatar_dialog->GetSelectedAvatar().toImage()); + } +} + +bool ConfigureProfileManager::LoadAvatarData() { + constexpr u64 AvatarImageDataId = 0x010000000000080AULL; + + // Attempt to load avatar data archive from installed firmware + auto* bis_system = system.GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install the firmware to use firmware avatars.")); + return false; + } + const auto nca = bis_system->GetEntry(AvatarImageDataId, FileSys::ContentRecordType::Data); + if (!nca) { + QMessageBox::warning(this, tr("Error loading archive"), + tr("Archive is not available. Please install/reinstall firmware.")); + return false; + } + const auto romfs = nca->GetRomFS(); + if (!romfs) { + QMessageBox::warning(this, tr("Error loading archive"), + tr("Archive does not contain romfs. It is probably corrupt.")); + return false; + } + const auto extracted = FileSys::ExtractRomFS(romfs); + if (!extracted) { + QMessageBox::warning(this, tr("Error extracting archive"), + tr("Archive could not be extracted. It is probably corrupt.")); + return false; + } + const auto chara_dir = extracted->GetSubdirectory("chara"); + if (!chara_dir) { + QMessageBox::warning(this, tr("Error finding image directory"), + tr("Failed to find image directory in the archive.")); + return false; + } + + QVector images; + for (const auto& item : chara_dir->GetFiles()) { + if (item->GetExtension() != "szs") { + continue; + } + + auto image_data = DecompressYaz0(item); + if (image_data.empty()) { + continue; + } + QImage image(reinterpret_cast(image_data.data()), 256, 256, + QImage::Format_RGBA8888); + images.append(QPixmap::fromImage(image)); + } + + if (images.isEmpty()) { + QMessageBox::warning(this, tr("No images found"), + tr("No avatar images were found in the archive.")); + return false; + } + + // Load the image data into the dialog + avatar_dialog->LoadImages(images); + return true; +} + +ConfigureProfileManagerAvatarDialog::ConfigureProfileManagerAvatarDialog(QWidget* parent) + : QDialog{parent}, avatar_list{new QListWidget(this)}, bg_color_button{new QPushButton(this)} { + auto* main_layout = new QVBoxLayout(this); + auto* button_layout = new QHBoxLayout(this); + auto* select_button = new QPushButton(tr("Select"), this); + auto* cancel_button = new QPushButton(tr("Cancel"), this); + auto* bg_color_label = new QLabel(tr("Background Color"), this); + + SetBackgroundColor(Qt::white); + + avatar_list->setViewMode(QListView::IconMode); + avatar_list->setIconSize(QSize(64, 64)); + avatar_list->setSpacing(4); + avatar_list->setResizeMode(QListView::Adjust); + avatar_list->setSelectionMode(QAbstractItemView::SingleSelection); + avatar_list->setEditTriggers(QAbstractItemView::NoEditTriggers); + avatar_list->setDragDropMode(QAbstractItemView::NoDragDrop); + avatar_list->setDragEnabled(false); + avatar_list->setDropIndicatorShown(false); + avatar_list->setAcceptDrops(false); + + button_layout->addWidget(bg_color_button); + button_layout->addWidget(bg_color_label); + button_layout->addStretch(); + button_layout->addWidget(select_button); + button_layout->addWidget(cancel_button); + + this->setLayout(main_layout); + this->setWindowTitle(tr("Select Firmware Avatar")); + main_layout->addWidget(avatar_list); + main_layout->addLayout(button_layout); + + connect(bg_color_button, &QPushButton::clicked, this, [this]() { + const auto new_color = QColorDialog::getColor(avatar_bg_color); + if (new_color.isValid()) { + SetBackgroundColor(new_color); + RefreshAvatars(); + } + }); + connect(select_button, &QPushButton::clicked, this, [this]() { accept(); }); + connect(cancel_button, &QPushButton::clicked, this, [this]() { reject(); }); +} + +ConfigureProfileManagerAvatarDialog::~ConfigureProfileManagerAvatarDialog() = default; + +void ConfigureProfileManagerAvatarDialog::SetBackgroundColor(const QColor& color) { + avatar_bg_color = color; + + bg_color_button->setStyleSheet( + QStringLiteral("background-color: %1; min-width: 60px;").arg(avatar_bg_color.name())); +} + +QPixmap ConfigureProfileManagerAvatarDialog::CreateAvatar(const QPixmap& avatar) { + QPixmap output(avatar.size()); + output.fill(avatar_bg_color); + + // Scale the image and fill it black to become our shadow + QPixmap shadow_pixmap = avatar.transformed(QTransform::fromScale(1.04, 1.04)); + QPainter shadow_painter(&shadow_pixmap); + shadow_painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + shadow_painter.fillRect(shadow_pixmap.rect(), Qt::black); + shadow_painter.end(); + + QPainter painter(&output); + painter.setOpacity(0.10); + painter.drawPixmap(0, 0, shadow_pixmap); + painter.setOpacity(1.0); + painter.drawPixmap(0, 0, avatar); + painter.end(); + + return output; +} + +void ConfigureProfileManagerAvatarDialog::RefreshAvatars() { + if (avatar_list->count() != avatar_image_store.size()) { + return; + } + for (int i = 0; i < avatar_image_store.size(); ++i) { + const auto icon = + QIcon(CreateAvatar(avatar_image_store[i]) + .scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + avatar_list->item(i)->setIcon(icon); + } +} + +void ConfigureProfileManagerAvatarDialog::LoadImages(const QVector& avatar_images) { + avatar_image_store = avatar_images; + avatar_list->clear(); + + for (int i = 0; i < avatar_image_store.size(); ++i) { + avatar_list->addItem(new QListWidgetItem); + } + RefreshAvatars(); + + // Determine window size now that avatars are loaded into the grid + // There is probably a better way to handle this that I'm unaware of + const auto* style = avatar_list->style(); + + const int icon_size = avatar_list->iconSize().width(); + const int icon_spacing = avatar_list->spacing() * 2; + const int icon_margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); + const int icon_full_size = icon_size + icon_spacing + icon_margin; + + const int horizontal_margin = style->pixelMetric(QStyle::PM_LayoutLeftMargin) + + style->pixelMetric(QStyle::PM_LayoutRightMargin) + + style->pixelMetric(QStyle::PM_ScrollBarExtent); + const int vertical_margin = style->pixelMetric(QStyle::PM_LayoutTopMargin) + + style->pixelMetric(QStyle::PM_LayoutBottomMargin); + + // Set default list size so that it is 6 icons wide and 4.5 tall + const int columns = 6; + const double rows = 4.5; + const int total_width = icon_full_size * columns + horizontal_margin; + const int total_height = icon_full_size * rows + vertical_margin; + avatar_list->setMinimumSize(total_width, total_height); +} + +bool ConfigureProfileManagerAvatarDialog::AreImagesLoaded() const { + return !avatar_image_store.isEmpty(); +} + +QPixmap ConfigureProfileManagerAvatarDialog::GetSelectedAvatar() { + return CreateAvatar(avatar_image_store[avatar_list->currentRow()]); +} + ConfigureProfileManagerDeleteDialog::ConfigureProfileManagerDeleteDialog(QWidget* parent) : QDialog{parent} { auto dialog_vbox_layout = new QVBoxLayout(this); @@ -374,3 +583,75 @@ void ConfigureProfileManagerDeleteDialog::SetInfo(const QString& username, const accept_callback(); }); } + +std::vector ConfigureProfileManager::DecompressYaz0(const FileSys::VirtualFile& file) { + if (!file) { + throw std::invalid_argument("Null file pointer passed to DecompressYaz0"); + } + + uint32_t magic{}; + file->ReadObject(&magic, 0); + if (magic != Common::MakeMagic('Y', 'a', 'z', '0')) { + return std::vector(); + } + + uint32_t decoded_length{}; + file->ReadObject(&decoded_length, 4); + decoded_length = Common::swap32(decoded_length); + + std::size_t input_size = file->GetSize() - 16; + std::vector input(input_size); + file->ReadBytes(input.data(), input_size, 16); + + uint32_t input_offset{}; + uint32_t output_offset{}; + std::vector output(decoded_length); + + uint16_t mask{}; + uint8_t header{}; + + while (output_offset < decoded_length) { + if ((mask >>= 1) == 0) { + header = input[input_offset++]; + mask = 0x80; + } + + if ((header & mask) != 0) { + if (output_offset == output.size()) { + break; + } + output[output_offset++] = input[input_offset++]; + } else { + uint8_t byte1 = input[input_offset++]; + uint8_t byte2 = input[input_offset++]; + + uint32_t dist = ((byte1 & 0xF) << 8) | byte2; + uint32_t position = output_offset - (dist + 1); + + uint32_t length = byte1 >> 4; + if (length == 0) { + length = static_cast(input[input_offset++]) + 0x12; + } else { + length += 2; + } + + uint32_t gap = output_offset - position; + uint32_t non_overlapping_length = length; + + if (non_overlapping_length > gap) { + non_overlapping_length = gap; + } + + std::memcpy(&output[output_offset], &output[position], non_overlapping_length); + output_offset += non_overlapping_length; + position += non_overlapping_length; + length -= non_overlapping_length; + + while (length-- > 0) { + output[output_offset++] = output[position++]; + } + } + } + + return output; +} \ No newline at end of file diff --git a/src/yuzu/configuration/configure_profile_manager.h b/src/yuzu/configuration/configure_profile_manager.h index 39560fdd9e..00dcdd5d9f 100644 --- a/src/yuzu/configuration/configure_profile_manager.h +++ b/src/yuzu/configuration/configure_profile_manager.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2016 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -25,6 +28,7 @@ class QStandardItem; class QStandardItemModel; class QTreeView; class QVBoxLayout; +class QListWidget; namespace Service::Account { class ProfileManager; @@ -34,6 +38,26 @@ namespace Ui { class ConfigureProfileManager; } +class ConfigureProfileManagerAvatarDialog : public QDialog { +public: + explicit ConfigureProfileManagerAvatarDialog(QWidget* parent); + ~ConfigureProfileManagerAvatarDialog(); + + void LoadImages(const QVector& avatar_images); + bool AreImagesLoaded() const; + QPixmap GetSelectedAvatar(); + +private: + void SetBackgroundColor(const QColor& color); + QPixmap CreateAvatar(const QPixmap& avatar); + void RefreshAvatars(); + + QVector avatar_image_store; + QListWidget* avatar_list; + QColor avatar_bg_color; + QPushButton* bg_color_button; +}; + class ConfigureProfileManagerDeleteDialog : public QDialog { public: explicit ConfigureProfileManagerDeleteDialog(QWidget* parent); @@ -71,13 +95,18 @@ private: void RenameUser(); void ConfirmDeleteUser(); void DeleteUser(const Common::UUID& uuid); - void SetUserImage(); + void SetUserImage(const QImage& image); + void SelectImageFile(); + void SelectFirmwareAvatar(); + bool LoadAvatarData(); + std::vector DecompressYaz0(const FileSys::VirtualFile& file); QVBoxLayout* layout; QTreeView* tree_view; QStandardItemModel* item_model; QGraphicsScene* scene; + ConfigureProfileManagerAvatarDialog* avatar_dialog; ConfigureProfileManagerDeleteDialog* confirm_dialog; std::vector> list_items; diff --git a/src/yuzu/configuration/configure_profile_manager.ui b/src/yuzu/configuration/configure_profile_manager.ui index bd6dea4f4b..d84931de02 100644 --- a/src/yuzu/configuration/configure_profile_manager.ui +++ b/src/yuzu/configuration/configure_profile_manager.ui @@ -117,6 +117,16 @@ + + + + false + + + Select Avatar + + + @@ -176,6 +186,6 @@ - - - + + + \ No newline at end of file