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