From e601b5ab878c732d1974f24e668435262e48b382 Mon Sep 17 00:00:00 2001 From: Gliniak Date: Sat, 10 May 2025 12:52:05 +0200 Subject: [PATCH] [UI] Added modify profile UI - Changed default icons size from 75x75 to 64x64 to match console icons size - Added png_utils - Removed all dialogs from xam_ui.cc into separate entities under xam/ui - Added option to return INT32 and WSTRING type settings to host - Added multiple enums related to user settings - Added code to handle changing user pfp - Fixed bug with system app being added to GPD played list - Changed logic in: XamUserGetUserFlagsFromXUID - Implemented: XamUserIsOnlineEnabled - Implemented: XamUserGetMembershipTier - Implemented: XamUserGetMembershipTierFromXUID - Implemented: XamUserGetUserTenure - Partially Implemented: XamUserGetSubscriptionType --- src/xenia/app/profile_dialogs.cc | 237 ++- src/xenia/app/profile_dialogs.h | 29 +- src/xenia/base/png_utils.cc | 68 + src/xenia/base/png_utils.h | 27 + src/xenia/kernel/xam/profile_manager.cc | 37 +- src/xenia/kernel/xam/profile_manager.h | 6 +- src/xenia/kernel/xam/ui/create_profile_ui.cc | 78 + src/xenia/kernel/xam/ui/create_profile_ui.h | 44 + .../kernel/xam/ui/game_achievements_ui.cc | 229 +++ .../kernel/xam/ui/game_achievements_ui.h | 63 + src/xenia/kernel/xam/ui/gamercard_ui.cc | 667 ++++++++ src/xenia/kernel/xam/ui/gamercard_ui.h | 91 ++ src/xenia/kernel/xam/ui/passcode_ui.cc | 101 ++ src/xenia/kernel/xam/ui/passcode_ui.h | 66 + src/xenia/kernel/xam/ui/signin_ui.cc | 247 +++ src/xenia/kernel/xam/ui/signin_ui.h | 53 + src/xenia/kernel/xam/ui/title_info_ui.cc | 229 +++ src/xenia/kernel/xam/ui/title_info_ui.h | 53 + src/xenia/kernel/xam/user_data.h | 23 + src/xenia/kernel/xam/user_profile.cc | 27 +- src/xenia/kernel/xam/user_profile.h | 14 + src/xenia/kernel/xam/user_settings.h | 53 +- src/xenia/kernel/xam/user_tracker.cc | 34 + src/xenia/kernel/xam/user_tracker.h | 8 +- src/xenia/kernel/xam/xam_ui.cc | 1447 +++-------------- src/xenia/kernel/xam/xam_ui.h | 126 ++ src/xenia/kernel/xam/xam_user.cc | 93 +- src/xenia/kernel/xam/xdbf/spa_info.h | 2 + src/xenia/ui/imgui_drawer.h | 2 +- src/xenia/xbox.h | 154 +- 30 files changed, 2904 insertions(+), 1404 deletions(-) create mode 100644 src/xenia/base/png_utils.cc create mode 100644 src/xenia/base/png_utils.h create mode 100644 src/xenia/kernel/xam/ui/create_profile_ui.cc create mode 100644 src/xenia/kernel/xam/ui/create_profile_ui.h create mode 100644 src/xenia/kernel/xam/ui/game_achievements_ui.cc create mode 100644 src/xenia/kernel/xam/ui/game_achievements_ui.h create mode 100644 src/xenia/kernel/xam/ui/gamercard_ui.cc create mode 100644 src/xenia/kernel/xam/ui/gamercard_ui.h create mode 100644 src/xenia/kernel/xam/ui/passcode_ui.cc create mode 100644 src/xenia/kernel/xam/ui/passcode_ui.h create mode 100644 src/xenia/kernel/xam/ui/signin_ui.cc create mode 100644 src/xenia/kernel/xam/ui/signin_ui.h create mode 100644 src/xenia/kernel/xam/ui/title_info_ui.cc create mode 100644 src/xenia/kernel/xam/ui/title_info_ui.h create mode 100644 src/xenia/kernel/xam/xam_ui.h diff --git a/src/xenia/app/profile_dialogs.cc b/src/xenia/app/profile_dialogs.cc index 82080b4aa..322f8db35 100644 --- a/src/xenia/app/profile_dialogs.cc +++ b/src/xenia/app/profile_dialogs.cc @@ -7,82 +7,24 @@ ****************************************************************************** */ -#include "xenia/app/profile_dialogs.h" #include + #include "xenia/app/emulator_window.h" +#include "xenia/app/profile_dialogs.h" +#include "xenia/base/png_utils.h" #include "xenia/base/system.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/xam_ui.h" +#include "xenia/ui/file_picker.h" + +#include "xenia/kernel/xam/ui/create_profile_ui.h" +#include "xenia/kernel/xam/ui/gamercard_ui.h" +#include "xenia/kernel/xam/ui/signin_ui.h" +#include "xenia/kernel/xam/ui/title_info_ui.h" namespace xe { -namespace kernel { -namespace xam { -extern bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, - const uint64_t xuid, const uint8_t user_index, - const X_XAMACCOUNTINFO* account, - uint64_t* selected_xuid); -} -} // namespace kernel namespace app { -void CreateProfileDialog::OnDraw(ImGuiIO& io) { - if (!has_opened_) { - ImGui::OpenPopup("Create Profile"); - has_opened_ = true; - } - - auto profile_manager = emulator_window_->emulator() - ->kernel_state() - ->xam_state() - ->profile_manager(); - - bool dialog_open = true; - if (!ImGui::BeginPopupModal("Create Profile", &dialog_open, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - Close(); - return; - } - - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - !ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0)) { - ImGui::SetKeyboardFocusHere(0); - } - - ImGui::TextUnformatted("Gamertag:"); - ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); - - const std::string gamertag_string = std::string(gamertag_); - bool valid = profile_manager->IsGamertagValid(gamertag_string); - - ImGui::BeginDisabled(!valid); - if (ImGui::Button("Create")) { - bool autologin = (profile_manager->GetAccountCount() == 0); - if (profile_manager->CreateProfile(gamertag_string, autologin, - migration_) && - migration_) { - emulator_window_->emulator()->DataMigration(0xB13EBABEBABEBABE); - } - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - ImGui::EndDisabled(); - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - - if (!dialog_open) { - ImGui::CloseCurrentPopup(); - Close(); - ImGui::EndPopup(); - return; - } - ImGui::EndPopup(); -} - void NoProfileDialog::OnDraw(ImGuiIO& io) { auto profile_manager = emulator_window_->emulator() ->kernel_state() @@ -124,13 +66,13 @@ void NoProfileDialog::OnDraw(ImGuiIO& io) { if (content_files.empty()) { if (ImGui::Button("Create Profile")) { - new CreateProfileDialog(emulator_window_->imgui_drawer(), - emulator_window_); + new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(), + emulator_window_->emulator()); } } else { if (ImGui::Button("Create profile & migrate data")) { - new CreateProfileDialog(emulator_window_->imgui_drawer(), - emulator_window_, true); + new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(), + emulator_window_->emulator(), true); } } @@ -149,6 +91,57 @@ void NoProfileDialog::OnDraw(ImGuiIO& io) { ImGui::End(); } +void ProfileConfigDialog::LoadProfileIcon() { + if (!emulator_window_) { + return; + } + + for (uint8_t user_index = 0; user_index < XUserMaxUserCount; user_index++) { + const auto profile = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager() + ->GetProfile(user_index); + + if (!profile) { + continue; + } + LoadProfileIcon(profile->xuid()); + } +} + +void ProfileConfigDialog::LoadProfileIcon(const uint64_t xuid) { + if (!emulator_window_) { + return; + } + + const auto profile_manager = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager(); + if (!profile_manager) { + return; + } + + const auto profile = profile_manager->GetProfile(xuid); + + if (!profile) { + if (profile_icon_.contains(xuid)) { + profile_icon_[xuid].release(); + } + return; + } + + const auto profile_icon = + profile->GetProfileIcon(kernel::xam::XTileType::kGamerTile); + if (profile_icon.empty()) { + return; + } + + profile_icon_[xuid].release(); + profile_icon_[xuid] = imgui_drawer()->LoadImGuiIcon(profile_icon); +} + void ProfileConfigDialog::OnDraw(ImGuiIO& io) { if (!emulator_window_->emulator() || !emulator_window_->emulator()->kernel_state() || @@ -184,28 +177,120 @@ void ProfileConfigDialog::OnDraw(ImGuiIO& io) { ImGui::Separator(); } + const ImVec2 next_window_position = + ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f, + ImGui::GetWindowPos().y); + for (auto& [xuid, account] : *profiles) { ImGui::PushID(static_cast(xuid)); const uint8_t user_index = profile_manager->GetUserIndexAssignedToProfile(xuid); - if (!kernel::xam::xeDrawProfileContent(imgui_drawer(), xuid, user_index, - &account, &selected_xuid_)) { + const auto profile_icon = profile_icon_.find(xuid) != profile_icon_.cend() + ? profile_icon_[xuid].get() + : nullptr; + + auto context_menu_fun = [=, this]() -> bool { + if (ImGui::BeginPopupContextItem("Profile Menu")) { + //*selected_xuid = xuid; + if (user_index == XUserIndexAny) { + if (ImGui::MenuItem("Login")) { + profile_manager->Login(xuid); + if (!profile_manager->GetProfile(xuid) + ->GetProfileIcon(kernel::xam::XTileType::kGamerTile) + .empty()) { + LoadProfileIcon(xuid); + } + } + if (ImGui::BeginMenu("Login to slot:")) { + for (uint8_t i = 1; i <= XUserMaxUserCount; i++) { + if (ImGui::MenuItem(fmt::format("slot {}", i).c_str())) { + profile_manager->Login(xuid, i - 1); + } + } + ImGui::EndMenu(); + } + } else { + if (ImGui::MenuItem("Logout")) { + profile_manager->Logout(user_index); + LoadProfileIcon(xuid); + } + } + + if (ImGui::MenuItem("Modify")) { + new kernel::xam::ui::GamercardUI( + emulator_window_->window(), emulator_window_->imgui_drawer(), + emulator_window_->emulator()->kernel_state(), xuid); + } + + const bool is_signedin = profile_manager->GetProfile(xuid) != nullptr; + ImGui::BeginDisabled(!is_signedin); + if (ImGui::MenuItem("Show Played Titles")) { + new kernel::xam::ui::TitleListUI( + emulator_window_->imgui_drawer(), next_window_position, + profile_manager->GetProfile(user_index)); + } + ImGui::EndDisabled(); + + if (ImGui::MenuItem("Show Content Directory")) { + const auto path = profile_manager->GetProfileContentPath( + xuid, emulator_window_->emulator()->kernel_state()->title_id()); + + if (!std::filesystem::exists(path)) { + std::filesystem::create_directories(path); + } + + std::thread path_open(LaunchFileExplorer, path); + path_open.detach(); + } + + if (!emulator_window_->emulator()->is_title_open()) { + ImGui::Separator(); + if (ImGui::BeginMenu("Delete Profile")) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted( + fmt::format( + "You're about to delete profile: {} (XUID: {:016X}). " + "This will remove all data assigned to this profile " + "including savefiles. Are you sure?", + account.GetGamertagString(), xuid) + .c_str()); + ImGui::EndTooltip(); + + if (ImGui::MenuItem("Yes, delete it!")) { + profile_manager->DeleteProfile(xuid); + ImGui::EndMenu(); + ImGui::EndPopup(); + return false; + } + + ImGui::EndMenu(); + } + } + ImGui::EndPopup(); + } + return true; + }; + + if (!kernel::xam::xeDrawProfileContent( + imgui_drawer(), xuid, user_index, &account, profile_icon, + context_menu_fun, [=, this]() { LoadProfileIcon(xuid); }, + &selected_xuid_)) { ImGui::PopID(); ImGui::End(); return; } ImGui::PopID(); - ImGui::Spacing(); ImGui::Separator(); } ImGui::Spacing(); if (ImGui::Button("Create Profile")) { - new CreateProfileDialog(emulator_window_->imgui_drawer(), emulator_window_); + new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(), + emulator_window_->emulator()); } ImGui::End(); diff --git a/src/xenia/app/profile_dialogs.h b/src/xenia/app/profile_dialogs.h index d9f4b3c0c..23a880ba5 100644 --- a/src/xenia/app/profile_dialogs.h +++ b/src/xenia/app/profile_dialogs.h @@ -19,26 +19,6 @@ namespace app { class EmulatorWindow; -class CreateProfileDialog final : public ui::ImGuiDialog { - public: - CreateProfileDialog(ui::ImGuiDrawer* imgui_drawer, - EmulatorWindow* emulator_window, - bool with_migration = false) - : ui::ImGuiDialog(imgui_drawer), - emulator_window_(emulator_window), - migration_(with_migration) { - memset(gamertag_, 0, sizeof(gamertag_)); - } - - protected: - void OnDraw(ImGuiIO& io) override; - - bool has_opened_ = false; - bool migration_ = false; - char gamertag_[16] = ""; - EmulatorWindow* emulator_window_; -}; - class NoProfileDialog final : public ui::ImGuiDialog { public: NoProfileDialog(ui::ImGuiDrawer* imgui_drawer, @@ -55,12 +35,19 @@ class ProfileConfigDialog final : public ui::ImGuiDialog { public: ProfileConfigDialog(ui::ImGuiDrawer* imgui_drawer, EmulatorWindow* emulator_window) - : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) {} + : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) { + LoadProfileIcon(); + } protected: void OnDraw(ImGuiIO& io) override; private: + void LoadProfileIcon(); + void LoadProfileIcon(const uint64_t xuid); + + std::map> profile_icon_; + uint64_t selected_xuid_ = 0; EmulatorWindow* emulator_window_; }; diff --git a/src/xenia/base/png_utils.cc b/src/xenia/base/png_utils.cc new file mode 100644 index 000000000..8d2b22da7 --- /dev/null +++ b/src/xenia/base/png_utils.cc @@ -0,0 +1,68 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/base/png_utils.h" +#include "xenia/base/filesystem.h" + +#include "third_party/stb/stb_image.h" + +namespace xe { + +bool IsFilePngImage(const std::filesystem::path& file_path) { + FILE* file = xe::filesystem::OpenFile(file_path, "rb"); + if (!file) { + return false; + } + + constexpr uint8_t magic_size = 4; + char magic[magic_size]; + if (fread(&magic, 1, magic_size, file) != magic_size) { + return false; + } + + fclose(file); + + if (magic[1] != 'P' || magic[2] != 'N' || magic[3] != 'G') { + return false; + } + + return true; +} + +std::pair GetImageResolution( + const std::filesystem::path& file_path) { + FILE* file = xe::filesystem::OpenFile(file_path, "rb"); + if (!file) { + return {}; + } + + int width, height, channels; + if (!stbi_info_from_file(file, &width, &height, &channels)) { + return {}; + } + + fclose(file); + return {width, height}; +} + +std::vector ReadPngFromFile(const std::filesystem::path& file_path) { + FILE* file = xe::filesystem::OpenFile(file_path, "rb"); + if (!file) { + return {}; + } + + const auto file_size = std::filesystem::file_size(file_path); + std::vector data(file_size); + fread(data.data(), 1, file_size, file); + fclose(file); + + return data; +} + +} // namespace xe diff --git a/src/xenia/base/png_utils.h b/src/xenia/base/png_utils.h new file mode 100644 index 000000000..b4a812a8d --- /dev/null +++ b/src/xenia/base/png_utils.h @@ -0,0 +1,27 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_BASE_PNG_UTILS_H_ +#define XENIA_BASE_PNG_UTILS_H_ + +#include +#include +#include + +namespace xe { + +bool IsFilePngImage(const std::filesystem::path& file_path); +std::pair GetImageResolution( + const std::filesystem::path& file_path); + +std::vector ReadPngFromFile(const std::filesystem::path& file_path); + +} // namespace xe + +#endif // XENIA_BASE_PNG_UTILS_H_ diff --git a/src/xenia/kernel/xam/profile_manager.cc b/src/xenia/kernel/xam/profile_manager.cc index 154983f77..199ef1c9c 100644 --- a/src/xenia/kernel/xam/profile_manager.cc +++ b/src/xenia/kernel/xam/profile_manager.cc @@ -10,9 +10,7 @@ #include "xenia/kernel/xam/profile_manager.h" #include -#include -#include "third_party/fmt/include/fmt/format.h" #include "xenia/base/logging.h" #include "xenia/emulator.h" #include "xenia/hid/input_system.h" @@ -139,6 +137,14 @@ ProfileManager::ProfileManager(KernelState* kernel_state, ProfileManager::~ProfileManager() {} +void ProfileManager::ReloadProfile(const uint64_t xuid) { + if (accounts_.contains(xuid)) { + accounts_.erase(xuid); + } + + LoadAccount(xuid); +} + void ProfileManager::ReloadProfiles() { LoadAccounts(FindProfiles()); } UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const { @@ -226,7 +232,7 @@ bool ProfileManager::LoadAccount(const uint64_t xuid) { // We need it only when we want to login into this account! DismountProfile(xuid); - accounts_.insert({xuid, tmp_acct}); + accounts_.insert_or_assign(xuid, tmp_acct); return true; } @@ -236,26 +242,6 @@ void ProfileManager::LoadAccounts(const std::vector profiles_xuids) { } } -void ProfileManager::ModifyGamertag(const uint64_t xuid, std::string gamertag) { - if (!accounts_.count(xuid)) { - return; - } - - xe::X_XAMACCOUNTINFO* account = &accounts_[xuid]; - - std::u16string gamertag_u16 = xe::to_utf16(gamertag); - - string_util::copy_truncating(account->gamertag, gamertag_u16, - sizeof(account->gamertag)); - - if (!MountProfile(xuid)) { - return; - } - - UpdateAccount(xuid, account); - DismountProfile(xuid); -} - bool ProfileManager::MountProfile(const uint64_t xuid, std::string mount_path) { std::filesystem::path profile_path = GetProfilePath(xuid); if (mount_path.empty()) { @@ -282,7 +268,7 @@ bool ProfileManager::DismountProfile(const uint64_t xuid) { void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index, bool notify) { - if (logged_profiles_.size() >= 4 && user_index >= XUserMaxUserCount) { + if (logged_profiles_.size() >= XUserMaxUserCount) { XELOGE( "Cannot login account with XUID: {:016X} due to lack of free slots " "(Max 4 accounts at once)", @@ -567,6 +553,9 @@ bool ProfileManager::UpdateAccount(const uint64_t xuid, std::span(encrypted_data.data(), encrypted_data.size()), 0, &written_bytes); output_file->Destroy(); + + // Refresh the in-memory account data + accounts_.insert_or_assign(xuid, *account); return true; } diff --git a/src/xenia/kernel/xam/profile_manager.h b/src/xenia/kernel/xam/profile_manager.h index d5d618bf6..1f7a341db 100644 --- a/src/xenia/kernel/xam/profile_manager.h +++ b/src/xenia/kernel/xam/profile_manager.h @@ -68,8 +68,6 @@ class ProfileManager { bool DeleteProfile(const uint64_t xuid); - void ModifyGamertag(const uint64_t xuid, std::string gamertag); - bool MountProfile(const uint64_t xuid, std::string mount_path = ""); bool DismountProfile(const uint64_t xuid); @@ -82,6 +80,7 @@ class ProfileManager { void LoadAccounts(const std::vector profiles_xuids); void ReloadProfiles(); + void ReloadProfile(const uint64_t xuid); UserProfile* GetProfile(const uint64_t xuid) const; UserProfile* GetProfile(const uint8_t user_index) const; @@ -101,13 +100,14 @@ class ProfileManager { const uint64_t xuid, const uint32_t title_id = -1, const XContentType content_type = XContentType::kInvalid) const; + bool UpdateAccount(const uint64_t xuid, const X_XAMACCOUNTINFO* account); + static bool IsGamertagValid(const std::string gamertag); private: void UpdateConfig(const uint64_t xuid, const uint8_t slot); bool CreateAccount(const uint64_t xuid, const std::string gamertag); bool CreateAccount(const uint64_t xuid, const X_XAMACCOUNTINFO* account); - bool UpdateAccount(const uint64_t xuid, const X_XAMACCOUNTINFO* account); std::filesystem::path GetProfilePath(const uint64_t xuid) const; std::filesystem::path GetProfilePath(const std::string xuid) const; diff --git a/src/xenia/kernel/xam/ui/create_profile_ui.cc b/src/xenia/kernel/xam/ui/create_profile_ui.cc new file mode 100644 index 000000000..ec641ae35 --- /dev/null +++ b/src/xenia/kernel/xam/ui/create_profile_ui.cc @@ -0,0 +1,78 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/create_profile_ui.h" +#include "xenia/emulator.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +void CreateProfileUI::OnDraw(ImGuiIO& io) { + if (!has_opened_) { + ImGui::OpenPopup("Create Profile"); + has_opened_ = true; + } + + auto profile_manager = + emulator_->kernel_state()->xam_state()->profile_manager(); + + bool dialog_open = true; + if (!ImGui::BeginPopupModal("Create Profile", &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + Close(); + return; + } + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + !ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + + ImGui::TextUnformatted("Gamertag:"); + ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); + + const std::string gamertag_string = std::string(gamertag_); + bool valid = profile_manager->IsGamertagValid(gamertag_string); + + ImGui::BeginDisabled(!valid); + if (ImGui::Button("Create")) { + bool autologin = (profile_manager->GetAccountCount() == 0); + if (profile_manager->CreateProfile(gamertag_string, autologin, + migration_) && + migration_) { + emulator_->DataMigration(0xB13EBABEBABEBABE); + } + std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); + dialog_open = false; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) { + std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); + dialog_open = false; + } + + if (!dialog_open) { + ImGui::CloseCurrentPopup(); + Close(); + ImGui::EndPopup(); + return; + } + ImGui::EndPopup(); +} + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/create_profile_ui.h b/src/xenia/kernel/xam/ui/create_profile_ui.h new file mode 100644 index 000000000..6356f7d45 --- /dev/null +++ b/src/xenia/kernel/xam/ui/create_profile_ui.h @@ -0,0 +1,44 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_CREATE_PROFILE_UI_H_ +#define XENIA_KERNEL_XAM_UI_CREATE_PROFILE_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +class CreateProfileUI final : public XamDialog { + public: + CreateProfileUI(xe::ui::ImGuiDrawer* imgui_drawer, Emulator* emulator, + bool with_migration = false) + : XamDialog(imgui_drawer), + emulator_(emulator), + migration_(with_migration) { + memset(gamertag_, 0, sizeof(gamertag_)); + } + + private: + void OnDraw(ImGuiIO& io) override; + + bool has_opened_ = false; + bool migration_ = false; + char gamertag_[16] = ""; + Emulator* emulator_; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/ui/game_achievements_ui.cc b/src/xenia/kernel/xam/ui/game_achievements_ui.cc new file mode 100644 index 000000000..6f527be67 --- /dev/null +++ b/src/xenia/kernel/xam/ui/game_achievements_ui.cc @@ -0,0 +1,229 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/game_achievements_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +GameAchievementsUI::GameAchievementsUI(xe::ui::ImGuiDrawer* imgui_drawer, + const ImVec2 drawing_position, + const TitleInfo* title_info, + const UserProfile* profile) + : XamDialog(imgui_drawer), + drawing_position_(drawing_position), + title_info_(*title_info), + profile_(profile), + window_id_(GetWindowId()) { + LoadAchievementsData(); +} + +GameAchievementsUI::~GameAchievementsUI() { + for (auto& entry : achievements_icons_) { + entry.second.release(); + } +} + +bool GameAchievementsUI::LoadAchievementsData() { + achievements_info_ = + kernel_state()->xam_state()->achievement_manager()->GetTitleAchievements( + profile_->xuid(), title_info_.id); + + if (achievements_info_.empty()) { + return false; + } + + xe::ui::IconsData data; + + for (const Achievement& entry : achievements_info_) { + const auto icon = + kernel_state()->xam_state()->achievement_manager()->GetAchievementIcon( + profile_->xuid(), title_info_.id, entry.achievement_id); + + data.insert({entry.image_id, icon}); + } + + achievements_icons_ = imgui_drawer()->LoadIcons(data); + return true; +} + +std::string GameAchievementsUI::GetAchievementTitle( + const Achievement& achievement_entry) const { + std::string title = "Secret trophy"; + + if (achievement_entry.IsUnlocked() || show_locked_info_ || + achievement_entry.flags & + static_cast(AchievementFlags::kShowUnachieved)) { + title = xe::to_utf8(achievement_entry.achievement_name); + } + + return title; +} + +std::string GameAchievementsUI::GetAchievementDescription( + const Achievement& achievement_entry) const { + std::string description = "Hidden description"; + + if (achievement_entry.flags & + static_cast(AchievementFlags::kShowUnachieved)) { + description = xe::to_utf8(achievement_entry.locked_description); + } + + if (achievement_entry.IsUnlocked() || show_locked_info_) { + description = xe::to_utf8(achievement_entry.unlocked_description); + } + + return description; +} + +xe::ui::ImmediateTexture* GameAchievementsUI::GetIcon( + const Achievement& achievement_entry) const { + if (!achievement_entry.IsUnlocked() && !show_locked_info_) { + return imgui_drawer()->GetLockedAchievementIcon(); + } + + if (achievements_icons_.count(achievement_entry.image_id)) { + return achievements_icons_.at(achievement_entry.image_id).get(); + } + + if (achievement_entry.IsUnlocked()) { + return nullptr; + } + return imgui_drawer()->GetLockedAchievementIcon(); +} + +std::string GameAchievementsUI::GetUnlockedTime( + const Achievement& achievement_entry) const { + if (achievement_entry.IsUnlockedOnline()) { + const auto unlock_time = chrono::WinSystemClock::to_local( + achievement_entry.unlock_time.to_time_point()); + + return fmt::format( + "Unlocked: {:%Y-%m-%d %H:%M}", + std::chrono::system_clock::time_point(unlock_time.time_since_epoch())); + } + + if (achievement_entry.unlock_time.is_valid()) { + const auto unlock_time = chrono::WinSystemClock::to_local( + achievement_entry.unlock_time.to_time_point()); + + return fmt::format( + "Unlocked: Offline ({:%Y-%m-%d %H:%M})", + std::chrono::system_clock::time_point(unlock_time.time_since_epoch())); + } + return fmt::format("Unlocked: Offline"); +} + +void GameAchievementsUI::DrawTitleAchievementInfo( + ImGuiIO& io, const Achievement& achievement_entry) const { + const auto start_drawing_pos = ImGui::GetCursorPos(); + + ImGui::TableSetColumnIndex(0); + + const auto icon = GetIcon(achievement_entry); + if (icon) { + ImGui::Image(reinterpret_cast(GetIcon(achievement_entry)), + xe::ui::default_image_icon_size); + } else { + ImGui::Dummy(xe::ui::default_image_icon_size); + } + ImGui::TableNextColumn(); + + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + const auto primary_line_height = ImGui::GetTextLineHeight(); + ImGui::Text("%s", GetAchievementTitle(achievement_entry).c_str()); + ImGui::PopFont(); + + ImGui::PushTextWrapPos(ImGui::GetMainViewport()->Size.x * 0.5f); + ImGui::TextWrapped("%s", + GetAchievementDescription(achievement_entry).c_str()); + ImGui::PopTextWrapPos(); + + ImGui::SetCursorPosY(start_drawing_pos.y + xe::ui::default_image_icon_size.x - + ImGui::GetTextLineHeight()); + + if (achievement_entry.IsUnlocked()) { + ImGui::Text("%s", GetUnlockedTime(achievement_entry).c_str()); + } + + ImGui::TableNextColumn(); + + // TODO(Gliniak): There is no easy way to align text to middle, so I have to + // do it manually. + const float achievement_row_middle_alignment = + ((xe::ui::default_image_icon_size.x / 2.f) - + ImGui::GetTextLineHeight() / 2.f) * + 0.85f; + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + + achievement_row_middle_alignment); + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + ImGui::TextUnformatted( + fmt::format("{} G", achievement_entry.gamerscore).c_str()); + ImGui::PopFont(); +} + +void GameAchievementsUI::OnDraw(ImGuiIO& io) { + ImGui::SetNextWindowPos(drawing_position_, ImGuiCond_FirstUseEver); + + const auto xenia_window_size = ImGui::GetMainViewport()->Size; + + ImGui::SetNextWindowSizeConstraints( + ImVec2(xenia_window_size.x * 0.2f, xenia_window_size.y * 0.3f), + ImVec2(xenia_window_size.x * 0.6f, xenia_window_size.y * 0.8f)); + ImGui::SetNextWindowBgAlpha(0.8f); + + bool dialog_open = true; + + std::string title_name = xe::to_utf8(title_info_.title_name); + title_name.erase(std::remove(title_name.begin(), title_name.end(), '\0'), + title_name.end()); + + const std::string window_name = + fmt::format("{} Achievements###{}", title_name, window_id_); + if (!ImGui::Begin(window_name.c_str(), &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + Close(); + ImGui::End(); + return; + } + + ImGui::Checkbox("Show locked achievements information", &show_locked_info_); + ImGui::Separator(); + + if (achievements_info_.empty()) { + ImGui::TextUnformatted(fmt::format("No achievements data!").c_str()); + } else { + if (ImGui::BeginTable("", 3, ImGuiTableFlags_BordersInnerH)) { + for (const auto& entry : achievements_info_) { + ImGui::TableNextRow(0, xe::ui::default_image_icon_size.y); + DrawTitleAchievementInfo(io, entry); + } + + ImGui::EndTable(); + } + } + + if (!dialog_open) { + Close(); + ImGui::End(); + return; + } + + ImGui::End(); +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/game_achievements_ui.h b/src/xenia/kernel/xam/ui/game_achievements_ui.h new file mode 100644 index 000000000..57499bf6e --- /dev/null +++ b/src/xenia/kernel/xam/ui/game_achievements_ui.h @@ -0,0 +1,63 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_GAME_ACHIEVEMENTS_UI_H_ +#define XENIA_KERNEL_XAM_UI_GAME_ACHIEVEMENTS_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +class GameAchievementsUI final : public XamDialog { + public: + GameAchievementsUI(xe::ui::ImGuiDrawer* imgui_drawer, + const ImVec2 drawing_position, const TitleInfo* title_info, + const UserProfile* profile); + + private: + ~GameAchievementsUI(); + bool LoadAchievementsData(); + + std::string GetAchievementTitle(const Achievement& achievement_entry) const; + + std::string GetAchievementDescription( + const Achievement& achievement_entry) const; + + xe::ui::ImmediateTexture* GetIcon(const Achievement& achievement_entry) const; + + std::string GetUnlockedTime(const Achievement& achievement_entry) const; + + void DrawTitleAchievementInfo(ImGuiIO& io, + const Achievement& achievement_entry) const; + + void OnDraw(ImGuiIO& io) override; + + private: + bool show_locked_info_ = false; + + uint64_t window_id_; + const ImVec2 drawing_position_ = {}; + + const TitleInfo title_info_; + const UserProfile* profile_; + + std::vector achievements_info_; + std::map> + achievements_icons_; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/ui/gamercard_ui.cc b/src/xenia/kernel/xam/ui/gamercard_ui.cc new file mode 100644 index 000000000..07f9fc1f2 --- /dev/null +++ b/src/xenia/kernel/xam/ui/gamercard_ui.cc @@ -0,0 +1,667 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/gamercard_ui.h" +#include "xenia/base/png_utils.h" +#include "xenia/ui/file_picker.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +constexpr float leftSideTextObjectAlignment = 100.f; +constexpr float rightSideTextObjectAlignment = 140.f; +// Because all these ENUM->NAME conversions will be used only here there is no +// reason to store them somewhere globally available. +static constexpr const char* XLanguageName[] = {nullptr, + "English", + "Japanese", + "German", + "French", + "Spanish", + "Italian", + "Korean", + "Traditional Chinese", + "Portuguese", + "Simplified Chinese", + "Polish", + "Russian"}; + +static constexpr const char* XOnlineCountry[] = {nullptr, + "United Arab Emirates", + "Albania", + "Armenia", + "Argentina", + "Austria", + "Australia", + "Azerbaijan", + "Belgium", + "Bulgaria", + "Bahrain", + "Brunei Darussalam", + "Bolivia", + "Brazil", + "Belarus", + "Belize", + "Canada", + nullptr, + "Switzerland", + "Chile", + "China", + "Colombia", + "Costa Rica", + "Czech Republic", + "Germany", + "Denmark", + "Dominican Republic", + "Algeria", + "Ecuador", + "Estonia", + "Egypt", + "Spain", + "Finland", + "Faroe Islands", + "France", + "Great Britain", + "Georgia", + "Greece", + "Guatemala", + "Hong Kong", + "Honduras", + "Croatia", + "Hungary", + "Indonesia", + "Ireland", + "Israel", + "India", + "Iraq", + "Iran", + "Iceland", + "Italy", + "Jamaica", + "Jordan", + "Japan", + "Kenya", + "Kyrgyzstan", + "Korea", + "Kuwait", + "Kazakhstan", + "Lebanon", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Latvia", + "Libya", + "Morocco", + "Monaco", + "Macedonia", + "Mongolia", + "Macau", + "Maldives", + "Mexico", + "Malaysia", + "Nicaragua", + "Netherlands", + "Norway", + "New Zealand", + "Oman", + "Panama", + "Peru", + "Philippines", + "Pakistan", + "Poland", + "Puerto Rico", + "Portugal", + "Paraguay", + "Qatar", + "Romania", + "Russian Federation", + "Saudi Arabia", + "Sweden", + "Singapore", + "Slovenia", + "Slovak Republic", + nullptr, + "El Salvador", + "Syria", + "Thailand", + "Tunisia", + "Turkey", + "Trinidad And Tobago", + "Taiwan", + "Ukraine", + "United States", + "Uruguay", + "Uzbekistan", + "Venezuela", + "Viet Nam", + "Yemen", + "South Africa", + "Zimbabwe"}; + +static constexpr const char* AccountSubscription[] = { + "None", nullptr, nullptr, "Silver", nullptr, + nullptr, "Gold", nullptr, nullptr, "Family"}; + +static constexpr const char* XGamerzoneName[] = {"None", "Recreation", "Pro", + "Family", "Underground"}; + +static constexpr const char* PreferredColorOptions[] = { + "None", "Black", "White", "Yellow", "Orange", "Pink", + "Red", "Purple", "Blue", "Green", "Brown", "Silver"}; + +static constexpr const char* ControllerVibrationOptions[] = {"Off", nullptr, + nullptr, "On"}; + +static constexpr const char* ControlSensitivityOptions[] = {"Medium", "Low", + "High"}; + +static constexpr const char* GamerDifficultyOptions[] = {"Normal", "Easy", + "Hard"}; + +static constexpr const char* AutoAimOptions[] = {"Off", "On"}; +static constexpr const char* AutoCenterOptions[] = {"Off", "On"}; +static constexpr const char* MovementControlOptions[] = {"Left Thumbstick", + "Right Thumbstick"}; +static constexpr const char* YAxisInversionOptions[] = {"Off", "On"}; +static constexpr const char* TransmissionOptions[] = {"Automatic", "Manual"}; +static constexpr const char* CameraLocationOptions[] = {"Behind", "In Front", + "Inside"}; +static constexpr const char* BrakeControlOptions[] = {"Trigger", "Button"}; +static constexpr const char* AcceleratorControlOptions[] = {"Trigger", + "Button"}; + +constexpr std::array UserSettingsToLoad = { + UserSettingId::XPROFILE_GAMER_TYPE, + UserSettingId::XPROFILE_GAMER_YAXIS_INVERSION, + UserSettingId::XPROFILE_OPTION_CONTROLLER_VIBRATION, + UserSettingId::XPROFILE_GAMERCARD_ZONE, + UserSettingId::XPROFILE_GAMERCARD_REGION, + UserSettingId::XPROFILE_GAMER_DIFFICULTY, + UserSettingId::XPROFILE_GAMER_CONTROL_SENSITIVITY, + UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_FIRST, + UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_SECOND, + UserSettingId::XPROFILE_GAMER_ACTION_AUTO_AIM, + UserSettingId::XPROFILE_GAMER_ACTION_AUTO_CENTER, + UserSettingId::XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL, + UserSettingId::XPROFILE_GAMER_RACE_TRANSMISSION, + UserSettingId::XPROFILE_GAMER_RACE_CAMERA_LOCATION, + UserSettingId::XPROFILE_GAMER_RACE_BRAKE_CONTROL, + UserSettingId::XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL, + UserSettingId::XPROFILE_GAMERCARD_USER_NAME, + UserSettingId::XPROFILE_GAMERCARD_USER_BIO, + UserSettingId::XPROFILE_GAMERCARD_MOTTO}; + +GamercardUI::GamercardUI(xe::ui::Window* window, + xe::ui::ImGuiDrawer* imgui_drawer, + KernelState* kernel_state, uint64_t xuid) + : XamDialog(imgui_drawer), + window_(window), + kernel_state_(kernel_state), + xuid_(xuid), + is_signed_in_(kernel_state->xam_state()->GetUserProfile(xuid) != + nullptr) { + LoadGamercardInfo(); +} + +void GamercardUI::LoadStringSetting(UserSettingId setting_id, char* buffer) { + const auto entry = xe::to_utf8(std::get( + gamercardOriginalValues_.gpd_settings[setting_id])); + + std::memcpy(buffer, entry.c_str(), entry.size()); +} + +void GamercardUI::LoadGamercardInfo() { + const auto account_data = + kernel_state()->xam_state()->profile_manager()->GetAccount(xuid_); + + std::memcpy(gamercardOriginalValues_.gamertag, + account_data->GetGamertagString().c_str(), + account_data->GetGamertagString().size()); + + gamercardOriginalValues_.country = account_data->GetCountry(); + gamercardOriginalValues_.language = account_data->GetLanguage(); + + gamercardOriginalValues_.is_live_enabled = account_data->IsLiveEnabled(); + + const std::string online_xuid_hex = + string_util::to_hex_string(account_data->GetOnlineXUID()); + + std::memcpy(gamercardOriginalValues_.online_xuid, online_xuid_hex.c_str(), + online_xuid_hex.size()); + + std::memcpy(gamercardOriginalValues_.online_domain, + account_data->GetOnlineDomain().data(), + account_data->GetOnlineDomain().size()); + + gamercardOriginalValues_.account_subscription_tier = + account_data->GetSubscriptionTier(); + + if (is_signed_in_) { + // GPD settings to load + for (const auto setting_id : UserSettingsToLoad) { + LoadSetting(setting_id); + } + + const auto gamer_icon = + kernel_state()->xam_state()->GetUserProfile(xuid_)->GetProfileIcon( + XTileType::kGamerTile); + + gamercardOriginalValues_.profile_icon.assign(gamer_icon.begin(), + gamer_icon.end()); + + gamercardOriginalValues_.icon_texture = + imgui_drawer()->LoadImGuiIcon(gamer_icon).release(); + + LoadStringSetting(UserSettingId::XPROFILE_GAMERCARD_USER_NAME, + gamercardOriginalValues_.gamer_name); + + LoadStringSetting(UserSettingId::XPROFILE_GAMERCARD_MOTTO, + gamercardOriginalValues_.gamer_motto); + + LoadStringSetting(UserSettingId::XPROFILE_GAMERCARD_USER_BIO, + gamercardOriginalValues_.gamer_bio); + } + + gamercardValues_ = gamercardOriginalValues_; +} + +void GamercardUI::LoadSetting(UserSettingId setting_id) { + const auto setting = kernel_state()->xam_state()->user_tracker()->GetSetting( + kernel_state()->xam_state()->GetUserProfile(xuid_), kDashboardID, + static_cast(setting_id)); + + if (!setting) { + return; + } + + gamercardOriginalValues_.gpd_settings[setting_id] = + setting.value().get_host_data(); +} + +void GamercardUI::DrawInputTextBox( + std::string label, char* buffer, size_t buffer_size, float alignment, + std::function)> on_input_change) { + ImGui::Text("%s", label.c_str()); + ImGui::SameLine(alignment); + + const ImVec2 input_field_pos = ImGui::GetCursorScreenPos(); + + ImGui::InputText(fmt::format("###{}", label).c_str(), buffer, buffer_size); + + if (on_input_change) { + if (!on_input_change({buffer, buffer_size})) { + const ImVec2 item_size = ImGui::GetItemRectSize(); + const ImVec2 end_pos = ImVec2(input_field_pos.x + item_size.x, + input_field_pos.y + item_size.y); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + draw_list->AddRect(input_field_pos, end_pos, IM_COL32(255, 0, 0, 200), + 0.0f, 0, 2.0f); + } + } +} + +void GamercardUI::DrawSettingComboBox(UserSettingId setting_id, + std::string label, + const char* const items[], int item_count, + float alignment) { + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(alignment); + + if (gamercardValues_.gpd_settings.contains(setting_id)) { + auto entry = &std::get(gamercardValues_.gpd_settings[setting_id]); + + ImGui::Combo(fmt::format("###{}", label).c_str(), + reinterpret_cast(entry), items, item_count); + } else { + ImGui::BeginDisabled(); + int empty = 0; + ImGui::Combo(fmt::format("###{}", label).c_str(), &empty, items, 0); + ImGui::EndDisabled(); + } +} + +void GamercardUI::SelectNewIcon() { + std::filesystem::path path; + + auto file_picker = xe::ui::FilePicker::Create(); + file_picker->set_mode(xe::ui::FilePicker::Mode::kOpen); + file_picker->set_type(xe::ui::FilePicker::Type::kFile); + file_picker->set_multi_selection(false); + file_picker->set_title("Select PNG Image"); + file_picker->set_extensions({{"PNG Image", "*.png"}}); + + if (file_picker->Show(window_)) { + auto selected_files = file_picker->selected_files(); + if (!selected_files.empty()) { + path = selected_files[0]; + } + + if (IsFilePngImage(path)) { + const auto res = GetImageResolution(path); + + if (res == kernel::xam::kProfileIconSizeSmall || + res == kernel::xam::kProfileIconSize) { + gamercardValues_.profile_icon = ReadPngFromFile(path); + gamercardValues_.icon_texture = + imgui_drawer() + ->LoadImGuiIcon(gamercardValues_.profile_icon) + .release(); + } + } + } +} + +void GamercardUI::DrawBaseSettings(ImGuiIO& io) { + ImGui::SeparatorText("Profile Settings"); + + DrawInputTextBox( + "Gamertag:", gamercardValues_.gamertag, + std::size(gamercardValues_.gamertag), leftSideTextObjectAlignment, + [](std::span data) { + return ProfileManager::IsGamertagValid(std::string(data.data())); + }); + + ImGui::BeginDisabled(!is_signed_in_); + ImGui::BeginDisabled(kernel_state_->title_id()); + if (ImGui::ImageButton( + "###ProfileIcon", + reinterpret_cast(gamercardValues_.icon_texture), + xe::ui::default_image_icon_size)) { + SelectNewIcon(); + } + ImGui::EndDisabled(); + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { + if (kernel_state_->title_id()) { + ImGui::SetTooltip("Icon change is disabled when title is running."); + } else { + ImGui::SetTooltip( + "Provide a PNG image with a resolution of 64x64. Icon will refresh " + "after relog."); + } + } + + ImGui::Text("Gamer Name:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::InputText("###GamerName", gamercardValues_.gamer_name, + std::size(gamercardValues_.gamer_name), + ImGuiInputTextFlags_ReadOnly); + + ImGui::Text("Gamer Motto:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::InputText("###GamerMotto", gamercardValues_.gamer_motto, + std::size(gamercardValues_.gamer_motto), + ImGuiInputTextFlags_ReadOnly); + + ImGui::Text("Gamer Bio:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::InputTextMultiline("###GamerBio", gamercardValues_.gamer_bio, + std::size(gamercardValues_.gamer_bio), ImVec2(), + ImGuiInputTextFlags_ReadOnly); + + ImGui::EndDisabled(); + + ImGui::Text("Language:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::Combo("###Language", + reinterpret_cast(&gamercardValues_.language), + XLanguageName, std::size(XLanguageName)); + + ImGui::Text("Country:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::Combo("###Country", reinterpret_cast(&gamercardValues_.country), + XOnlineCountry, std::size(XOnlineCountry)); +} + +void GamercardUI::DrawOnlineSettings(ImGuiIO& io) { + ImGui::SeparatorText("Online Profile Settings"); + + if (ImGui::Checkbox("Live Enabled", &gamercardValues_.is_live_enabled)) { + // TODO: Add checks to decide if online XUID generation is required + } + + ImGui::Text("Online XUID:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::InputText("###OnlineXUID", gamercardValues_.online_xuid, + std::size(gamercardValues_.online_xuid), + ImGuiInputTextFlags_ReadOnly); + + ImGui::Text("Online Domain:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::InputText("###OnlineDomain", gamercardValues_.online_domain, + std::size(gamercardValues_.online_domain), + ImGuiInputTextFlags_ReadOnly); + + ImGui::BeginDisabled(!gamercardValues_.is_live_enabled); + DrawSettingComboBox(UserSettingId::XPROFILE_GAMERCARD_ZONE, "Gamer Zone", + XGamerzoneName, std::size(XGamerzoneName), + leftSideTextObjectAlignment); + + ImGui::Text("Subscription Tier:"); + ImGui::SameLine(leftSideTextObjectAlignment); + ImGui::Combo( + "###Subscription", + reinterpret_cast(&gamercardValues_.account_subscription_tier), + AccountSubscription, std::size(AccountSubscription)); + + ImGui::EndDisabled(); +} + +void GamercardUI::DrawGpdSettings(ImGuiIO& io) { + ImGui::SeparatorText("Game Settings"); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_DIFFICULTY, "Difficulty", + GamerDifficultyOptions, std::size(GamerDifficultyOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_OPTION_CONTROLLER_VIBRATION, + "Controller Vibration", ControllerVibrationOptions, + std::size(ControllerVibrationOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_CONTROL_SENSITIVITY, + "Control Sensitivity", ControlSensitivityOptions, + std::size(ControlSensitivityOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_FIRST, + "Favorite Color (First)", PreferredColorOptions, + std::size(PreferredColorOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_SECOND, + "Favorite Color (Second)", PreferredColorOptions, + std::size(PreferredColorOptions), + rightSideTextObjectAlignment); + + ImGui::SeparatorText("Action Games Settings"); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_YAXIS_INVERSION, + "Y-axis Inversion", YAxisInversionOptions, + std::size(YAxisInversionOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_ACTION_AUTO_AIM, "Auto Aim", + AutoAimOptions, std::size(AutoAimOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_ACTION_AUTO_CENTER, + "Auto Center", AutoCenterOptions, + std::size(AutoCenterOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL, + "Movement Control", MovementControlOptions, + std::size(MovementControlOptions), + rightSideTextObjectAlignment); + + ImGui::SeparatorText("Racing Games Settings"); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_RACE_TRANSMISSION, + "Transmission", TransmissionOptions, + std::size(TransmissionOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_RACE_CAMERA_LOCATION, + "Camera Location", CameraLocationOptions, + std::size(CameraLocationOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_RACE_BRAKE_CONTROL, + "Brake Control", BrakeControlOptions, + std::size(BrakeControlOptions), + rightSideTextObjectAlignment); + + DrawSettingComboBox(UserSettingId::XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL, + "Accelerator Control", AcceleratorControlOptions, + std::size(AcceleratorControlOptions), + rightSideTextObjectAlignment); +} + +void GamercardUI::OnDraw(ImGuiIO& io) { + if (!has_opened_) { + ImGui::OpenPopup(fmt::format("{}'s Gamercard", + std::string(gamercardOriginalValues_.gamertag)) + .c_str()); + has_opened_ = true; + } + + bool dialog_open = true; + if (!ImGui::BeginPopupModal( + fmt::format("{}'s Gamercard", + std::string(gamercardOriginalValues_.gamertag)) + .c_str(), + &dialog_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + Close(); + return; + } + + if (ImGui::BeginTable("###GamercardTable", 2)) { + ImGui::TableSetupColumn("###Label", ImGuiTableColumnFlags_WidthFixed, + 350.0f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + DrawBaseSettings(io); + DrawOnlineSettings(io); + ImGui::TableNextColumn(); + DrawGpdSettings(io); + + ImGui::EndTable(); + } + + ImGui::NewLine(); + + const bool is_valid_gamertag = + ProfileManager::IsGamertagValid(std::string(gamercardValues_.gamertag)); + + ImGui::BeginDisabled(!is_valid_gamertag); + if (ImGui::Button("Save")) { + SaveProfileIcon(); + SaveSettings(); + SaveAccountData(); + dialog_open = false; + } + if (!is_valid_gamertag) { + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("Saving disabled! Invalid gamertag provided."); + } + + ImGui::EndDisabled(); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) { + dialog_open = false; + } + + if (!dialog_open) { + ImGui::CloseCurrentPopup(); + Close(); + ImGui::EndPopup(); + return; + } + ImGui::EndPopup(); +} + +void GamercardUI::SaveAccountData() { + const auto account_original = + *kernel_state()->xam_state()->profile_manager()->GetAccount(xuid_); + auto account = account_original; + + account.SetCountry(gamercardValues_.country); + account.SetLanguage(gamercardValues_.language); + account.SetSubscriptionTier(gamercardValues_.account_subscription_tier); + account.ToggleLiveFlag(gamercardValues_.is_live_enabled); + + std::u16string gamertag = + xe::to_utf16(std::string(gamercardValues_.gamertag)); + string_util::copy_truncating(account.gamertag, gamertag, + std::size(account.gamertag)); + + if (std::memcmp(&account, &account_original, sizeof(X_XAMACCOUNTINFO)) != 0) { + if (!is_signed_in_) { + kernel_state()->xam_state()->profile_manager()->MountProfile(xuid_); + } + + kernel_state()->xam_state()->profile_manager()->UpdateAccount(xuid_, + &account); + + if (!is_signed_in_) { + kernel_state()->xam_state()->profile_manager()->DismountProfile(xuid_); + } + } +} + +void GamercardUI::SaveProfileIcon() { + if (gamercardValues_.profile_icon == gamercardOriginalValues_.profile_icon) { + return; + } + + kernel_state()->xam_state()->user_tracker()->UpdateUserIcon( + xuid_, {gamercardValues_.profile_icon.data(), + gamercardValues_.profile_icon.size()}); +} + +void GamercardUI::SaveSettings() { + // First check all GPD embedded settings. + for (const auto& [id, setting] : gamercardValues_.gpd_settings) { + // No change was made to the setting? + if (gamercardOriginalValues_.gpd_settings[id] == setting) { + continue; + } + + UserSetting updated_setting(id, setting); + kernel_state()->xam_state()->user_tracker()->UpsertSetting( + xuid_, kDashboardID, &updated_setting); + } + + const uint32_t user_index = kernel_state() + ->xam_state() + ->profile_manager() + ->GetUserIndexAssignedToProfile(xuid_); + + if (user_index != XUserIndexAny) { + // TODO: Fix it to update only changed player + kernel_state_->BroadcastNotification( + kXNotificationSystemProfileSettingChanged, 0xF); + } +} + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/gamercard_ui.h b/src/xenia/kernel/xam/ui/gamercard_ui.h new file mode 100644 index 000000000..755098b11 --- /dev/null +++ b/src/xenia/kernel/xam/ui/gamercard_ui.h @@ -0,0 +1,91 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_GAMERCARD_UI_H_ +#define XENIA_KERNEL_XAM_UI_GAMERCARD_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +struct GamercardSettings { + // Account settings + char gamertag[16]; + xe::XOnlineCountry country; + xe::XLanguage language; + bool is_live_enabled; + char online_xuid[0x11]; + char online_domain[0x14]; + X_XAMACCOUNTINFO::AccountSubscriptionTier account_subscription_tier; + + // GPD settings + std::map gpd_settings; + + // GPD string buffers + char gamer_name[0x104]; + char gamer_motto[0x2C]; + char gamer_bio[kMaxUserDataSize]; + + // Other + std::vector profile_icon; + // Immediate Texture? + xe::ui::ImmediateTexture* icon_texture; +}; + +class GamercardUI final : public XamDialog { + public: + GamercardUI(xe::ui::Window* window, xe::ui::ImGuiDrawer* imgui_drawer, + KernelState* kernel_state, uint64_t xuid); + + private: + void OnDraw(ImGuiIO& io) override; + void DrawBaseSettings(ImGuiIO& io); + void DrawOnlineSettings(ImGuiIO& io); + void DrawGpdSettings(ImGuiIO& io); + + void LoadGamercardInfo(); + void LoadSetting(UserSettingId setting_id); + void LoadStringSetting(UserSettingId setting_id, char* buffer); + void SaveSettings(); + void SaveAccountData(); + void SaveProfileIcon(); + + void DrawSettingComboBox(UserSettingId setting_id, std::string label, + const char* const items[], int item_count, + float alignment); + + void DrawInputTextBox( + std::string label, char* buffer, size_t buffer_size, float alignment, + std::function)> on_input_change = {}); + + void SelectNewIcon(); + + const uint64_t xuid_ = 0; + const bool is_signed_in_ = false; + const bool is_valid_gamertag_ = true; + + bool has_opened_ = false; + KernelState* kernel_state_; + xe::ui::Window* window_; + + // We're storing OG and current values to compare at the end and send what was + // changed. + GamercardSettings gamercardOriginalValues_ = {}; + GamercardSettings gamercardValues_ = {}; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/ui/passcode_ui.cc b/src/xenia/kernel/xam/ui/passcode_ui.cc new file mode 100644 index 000000000..5dd76d61c --- /dev/null +++ b/src/xenia/kernel/xam/ui/passcode_ui.cc @@ -0,0 +1,101 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/passcode_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +ProfilePasscodeUI::ProfilePasscodeUI(xe::ui::ImGuiDrawer* imgui_drawer, + std::string_view title, + std::string_view description, + MESSAGEBOX_RESULT* result_ptr) + : XamDialog(imgui_drawer), + title_(title), + description_(description), + result_ptr_(result_ptr) { + std::memset(result_ptr, 0, sizeof(MESSAGEBOX_RESULT)); + + if (title_.empty()) { + title_ = "Enter Pass Code"; + } + + if (description_.empty()) { + description_ = "Enter your Xbox LIVE pass code."; + } +} + +void ProfilePasscodeUI::DrawPasscodeField(uint8_t key_id) { + const std::string label = fmt::format("##Key {}", key_id); + + if (ImGui::BeginCombo(label.c_str(), labelled_keys_[key_indexes_[key_id]])) { + for (uint8_t key_index = 0; key_index < keys_map_.size(); key_index++) { + bool is_selected = key_id == key_index; + + if (ImGui::Selectable(labelled_keys_[key_index], is_selected)) { + key_indexes_[key_id] = key_index; + } + + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + + ImGui::EndCombo(); + } +} + +void ProfilePasscodeUI::OnDraw(ImGuiIO& io) { + if (!has_opened_) { + ImGui::OpenPopup(title_.c_str()); + has_opened_ = true; + } + + if (ImGui::BeginPopupModal(title_.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + if (description_.size()) { + ImGui::Text("%s", description_.c_str()); + } + + for (uint8_t i = 0; i < passcode_length; i++) { + DrawPasscodeField(i); + // result_ptr_->Passcode[i] = + // keys_map_.at(labelled_keys_[key_indexes_[i]]); + } + + ImGui::NewLine(); + + // We write each key on close to prevent simultaneous dialogs. + if (ImGui::Button("Sign In")) { + for (uint8_t i = 0; i < passcode_length; i++) { + result_ptr_->Passcode[i] = + keys_map_.at(labelled_keys_[key_indexes_[i]]); + } + + selected_signed_in_ = true; + + Close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) { + Close(); + } + } + + ImGui::EndPopup(); +} + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/passcode_ui.h b/src/xenia/kernel/xam/ui/passcode_ui.h new file mode 100644 index 000000000..a354ed07d --- /dev/null +++ b/src/xenia/kernel/xam/ui/passcode_ui.h @@ -0,0 +1,66 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_PASSCODE_UI_H_ +#define XENIA_KERNEL_XAM_UI_PASSCODE_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +class ProfilePasscodeUI final : public XamDialog { + public: + ProfilePasscodeUI(xe::ui::ImGuiDrawer* imgui_drawer, std::string_view title, + std::string_view description, + MESSAGEBOX_RESULT* result_ptr); + + void DrawPasscodeField(uint8_t key_id); + + void OnDraw(ImGuiIO& io) override; + + virtual ~ProfilePasscodeUI() {} + + bool SelectedSignedIn() const { return selected_signed_in_; } + + private: + const char* labelled_keys_[11] = {"None", "X", "Y", "RB", "LB", "LT", + "RT", "Up", "Down", "Left", "Right"}; + + const std::map keys_map_ = { + {"None", 0}, + {"X", X_BUTTON_PASSCODE}, + {"Y", Y_BUTTON_PASSCODE}, + {"RB", RIGHT_BUMPER_PASSCODE}, + {"LB", LEFT_BUMPER_PASSCODE}, + {"LT", LEFT_TRIGGER_PASSCODE}, + {"RT", RIGHT_TRIGGER_PASSCODE}, + {"Up", DPAD_UP_PASSCODE}, + {"Down", DPAD_DOWN_PASSCODE}, + {"Left", DPAD_LEFT_PASSCODE}, + {"Right", DPAD_RIGHT_PASSCODE}}; + + bool has_opened_ = false; + bool selected_signed_in_ = false; + std::string title_; + std::string description_; + + static constexpr uint8_t passcode_length = sizeof(X_XAMACCOUNTINFO::passcode); + int key_indexes_[passcode_length] = {0, 0, 0, 0}; + MESSAGEBOX_RESULT* result_ptr_; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/ui/signin_ui.cc b/src/xenia/kernel/xam/ui/signin_ui.cc new file mode 100644 index 000000000..7077e106c --- /dev/null +++ b/src/xenia/kernel/xam/ui/signin_ui.cc @@ -0,0 +1,247 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/signin_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +SigninUI::SigninUI(xe::ui::ImGuiDrawer* imgui_drawer, + ProfileManager* profile_manager, uint32_t last_used_slot, + uint32_t users_needed) + : XamDialog(imgui_drawer), + profile_manager_(profile_manager), + last_user_(last_used_slot), + users_needed_(users_needed), + title_("Sign In") {} + +void SigninUI::OnDraw(ImGuiIO& io) { + bool first_draw = false; + if (!has_opened_) { + ImGui::OpenPopup(title_.c_str()); + has_opened_ = true; + first_draw = true; + ReloadProfiles(true); + } + if (ImGui::BeginPopupModal(title_.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + for (uint32_t i = 0; i < users_needed_; i++) { + ImGui::BeginGroup(); + + std::vector combo_items; + int items_count = 0; + int current_item = 0; + + // Fill slot list. + std::vector slots; + slots.push_back(0xFF); + combo_items.push_back("---"); + for (auto& elem : slot_data_) { + // Select the slot or skip it if it's already used. + bool already_taken = false; + for (uint32_t j = 0; j < users_needed_; j++) { + if (chosen_slots_[j] == elem.first) { + if (i == j) { + current_item = static_cast(combo_items.size()); + } else { + already_taken = true; + } + break; + } + } + + if (already_taken) { + continue; + } + + slots.push_back(elem.first); + combo_items.push_back(elem.second.c_str()); + } + items_count = static_cast(combo_items.size()); + + ImGui::BeginDisabled(users_needed_ == 1); + ImGui::Combo(fmt::format("##Slot{:d}", i).c_str(), ¤t_item, + combo_items.data(), items_count); + chosen_slots_[i] = slots[current_item]; + ImGui::EndDisabled(); + ImGui::Spacing(); + + combo_items.clear(); + current_item = 0; + + // Fill profile list. + std::vector xuids; + xuids.push_back(0); + combo_items.push_back("---"); + if (chosen_slots_[i] != 0xFF) { + for (auto& elem : profile_data_) { + // Select the profile or skip it if it's already used. + bool already_taken = false; + for (uint32_t j = 0; j < users_needed_; j++) { + if (chosen_xuids_[j] == elem.first) { + if (i == j) { + current_item = static_cast(combo_items.size()); + } else { + already_taken = true; + } + break; + } + } + + if (already_taken) { + continue; + } + + xuids.push_back(elem.first); + combo_items.push_back(elem.second.c_str()); + } + } + items_count = static_cast(combo_items.size()); + + ImGui::BeginDisabled(chosen_slots_[i] == 0xFF); + ImGui::Combo(fmt::format("##Profile{:d}", i).c_str(), ¤t_item, + combo_items.data(), items_count); + chosen_xuids_[i] = xuids[current_item]; + ImGui::EndDisabled(); + ImGui::Spacing(); + + // Draw profile badge. + uint8_t slot = chosen_slots_[i]; + uint64_t xuid = chosen_xuids_[i]; + const auto account = profile_manager_->GetAccount(xuid); + + if (slot == 0xFF || xuid == 0 || !account) { + float ypos = ImGui::GetCursorPosY(); + ImGui::SetCursorPosY(ypos + ImGui::GetTextLineHeight() * 5); + } else { + xeDrawProfileContent(imgui_drawer(), xuid, slot, account, nullptr, {}, + {}, nullptr); + } + + ImGui::EndGroup(); + if (i != (users_needed_ - 1) && (i == 0 || i == 2)) { + ImGui::SameLine(); + } + } + + ImGui::Spacing(); + + if (ImGui::Button("Create Profile")) { + creating_profile_ = true; + ImGui::OpenPopup("Create Profile"); + first_draw = true; + } + ImGui::Spacing(); + + if (creating_profile_) { + if (ImGui::BeginPopupModal("Create Profile", nullptr, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + if (first_draw) { + ImGui::SetKeyboardFocusHere(); + } + + ImGui::TextUnformatted("Gamertag:"); + ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); + + const std::string gamertag_string = gamertag_; + bool valid = profile_manager_->IsGamertagValid(gamertag_string); + + ImGui::BeginDisabled(!valid); + if (ImGui::Button("Create")) { + profile_manager_->CreateProfile(gamertag_string, false); + std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); + ImGui::CloseCurrentPopup(); + creating_profile_ = false; + ReloadProfiles(false); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) { + std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); + ImGui::CloseCurrentPopup(); + creating_profile_ = false; + } + + ImGui::EndPopup(); + } else { + creating_profile_ = false; + } + } + + if (ImGui::Button("OK")) { + std::map profile_map; + for (uint32_t i = 0; i < users_needed_; i++) { + uint8_t slot = chosen_slots_[i]; + uint64_t xuid = chosen_xuids_[i]; + if (slot != 0xFF && xuid != 0) { + profile_map[slot] = xuid; + } + } + profile_manager_->LoginMultiple(profile_map); + + ImGui::CloseCurrentPopup(); + Close(); + } + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + Close(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::EndPopup(); + } else { + Close(); + } +} + +void SigninUI::ReloadProfiles(bool first_draw) { + auto profile_manager = kernel_state()->xam_state()->profile_manager(); + auto profiles = profile_manager->GetAccounts(); + + profile_data_.clear(); + for (auto& [xuid, account] : *profiles) { + profile_data_.push_back({xuid, account.GetGamertagString()}); + } + + if (first_draw) { + // If only one user is requested, request last used controller to sign in. + if (users_needed_ == 1) { + chosen_slots_[0] = last_user_; + } else { + for (uint32_t i = 0; i < users_needed_; i++) { + // TODO: Not sure about this, needs testing on real hardware. + chosen_slots_[i] = i; + } + } + + // Default profile selection to profile that is already signed in. + for (auto& elem : profile_data_) { + uint64_t xuid = elem.first; + uint8_t slot = profile_manager->GetUserIndexAssignedToProfile(xuid); + for (uint32_t j = 0; j < users_needed_; j++) { + if (chosen_slots_[j] != XUserIndexAny && slot == chosen_slots_[j]) { + chosen_xuids_[j] = xuid; + } + } + } + } +} + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/signin_ui.h b/src/xenia/kernel/xam/ui/signin_ui.h new file mode 100644 index 000000000..1af156ba1 --- /dev/null +++ b/src/xenia/kernel/xam/ui/signin_ui.h @@ -0,0 +1,53 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_SIGNIN_UI_H_ +#define XENIA_KERNEL_XAM_UI_SIGNIN_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +class SigninUI final : public XamDialog { + public: + SigninUI(xe::ui::ImGuiDrawer* imgui_drawer, ProfileManager* profile_manager, + uint32_t last_used_slot, uint32_t users_needed); + + private: + void OnDraw(ImGuiIO& io) override; + + void ReloadProfiles(bool first_draw); + + const std::map slot_data_ = { + {0, "Slot 0"}, {1, "Slot 1"}, {2, "Slot 2"}, {3, "Slot 3"}}; + + ProfileManager* profile_manager_ = nullptr; + + bool has_opened_ = false; + std::string title_; + uint32_t users_needed_ = 1; + uint32_t last_user_ = 0; + + std::vector> profile_data_; + uint8_t chosen_slots_[XUserMaxUserCount] = {}; + uint64_t chosen_xuids_[XUserMaxUserCount] = {}; + + bool creating_profile_ = false; + char gamertag_[16] = ""; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/ui/title_info_ui.cc b/src/xenia/kernel/xam/ui/title_info_ui.cc new file mode 100644 index 000000000..567ca709e --- /dev/null +++ b/src/xenia/kernel/xam/ui/title_info_ui.cc @@ -0,0 +1,229 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/ui/title_info_ui.h" +#include "xenia/kernel/xam/ui/game_achievements_ui.h" + +#include "xenia/base/system.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +TitleListUI::TitleListUI(xe::ui::ImGuiDrawer* imgui_drawer, + const ImVec2 drawing_position, + const UserProfile* profile) + : XamDialog(imgui_drawer), + drawing_position_(drawing_position), + profile_(profile), + profile_manager_(kernel_state()->xam_state()->profile_manager()), + dialog_name_( + fmt::format("{}'s Games List###{}", profile->name(), GetWindowId())) { + LoadProfileTitleList(imgui_drawer, profile); +} + +TitleListUI::~TitleListUI() { + for (auto& entry : title_icon) { + entry.second.release(); + } +} + +void TitleListUI::LoadProfileTitleList(xe::ui::ImGuiDrawer* imgui_drawer, + const UserProfile* profile) { + info_.clear(); + + xe::ui::IconsData data; + + info_ = kernel_state()->xam_state()->user_tracker()->GetPlayedTitles( + profile->xuid()); + for (const auto& title_info : info_) { + if (!title_info.icon.empty()) { + data[title_info.id] = title_info.icon; + } + } + + title_icon = imgui_drawer->LoadIcons(data); +} + +void TitleListUI::DrawTitleEntry(ImGuiIO& io, TitleInfo& entry) { + const auto start_position = ImGui::GetCursorPos(); + const ImVec2 next_window_position = + ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f, + ImGui::GetWindowPos().y); + + // First Column + ImGui::TableSetColumnIndex(0); + + if (title_icon.count(entry.id)) { + ImGui::Image(reinterpret_cast(title_icon.at(entry.id).get()), + xe::ui::default_image_icon_size); + } else { + ImGui::Dummy(xe::ui::default_image_icon_size); + } + + // Second Column + ImGui::TableNextColumn(); + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + ImGui::TextUnformatted(xe::to_utf8(entry.title_name).c_str()); + ImGui::PopFont(); + + ImGui::TextUnformatted( + fmt::format("{}/{} Achievements unlocked ({} Gamerscore)", + entry.unlocked_achievements_count, entry.achievements_count, + entry.title_earned_gamerscore) + .c_str()); + + ImGui::SetCursorPosY(start_position.y + xe::ui::default_image_icon_size.y - + ImGui::GetTextLineHeight()); + + if (entry.WasTitlePlayed()) { + ImGui::TextUnformatted( + fmt::format("Last played: {:%Y-%m-%d %H:%M}", + std::chrono::system_clock::time_point( + entry.last_played.time_since_epoch())) + .c_str()); + } else { + ImGui::TextUnformatted("Last played: Unknown"); + } + ImGui::TableNextColumn(); + + const ImVec2 end_draw_position = + ImVec2(ImGui::GetCursorPos().x - start_position.x, + ImGui::GetCursorPos().y - start_position.y); + + ImGui::SetCursorPos(start_position); + + if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(), + selected_title_ == entry.id, + ImGuiSelectableFlags_SpanAllColumns, + end_draw_position)) { + selected_title_ = entry.id; + new GameAchievementsUI(imgui_drawer(), next_window_position, &entry, + profile_); + } + + if (ImGui::BeginPopupContextItem( + fmt::format("Title Menu {:08X}", entry.id).c_str())) { + selected_title_ = entry.id; + if (ImGui::MenuItem("Refresh title stats", nullptr, nullptr, true)) { + kernel_state()->xam_state()->user_tracker()->RefreshTitleSummary( + profile_->xuid(), entry.id); + + const auto title_info = + kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo( + profile_->xuid(), entry.id); + + if (title_info) { + entry = title_info.value(); + } + } + + const auto savefile_path = profile_manager_->GetProfileContentPath( + profile_->xuid(), entry.id, XContentType::kSavedGame); + + const auto dlc_path = profile_manager_->GetProfileContentPath( + 0, entry.id, XContentType::kMarketplaceContent); + + const auto tu_path = profile_manager_->GetProfileContentPath( + 0, entry.id, XContentType::kInstaller); + + if (ImGui::MenuItem("Open savefile directory", nullptr, nullptr, + std::filesystem::exists(savefile_path))) { + std::thread path_open(LaunchFileExplorer, savefile_path); + path_open.detach(); + } + if (ImGui::MenuItem("Open DLC directory", nullptr, nullptr, + std::filesystem::exists(dlc_path))) { + std::thread path_open(LaunchFileExplorer, dlc_path); + path_open.detach(); + } + if (ImGui::MenuItem("Open Title Update directory", nullptr, nullptr, + std::filesystem::exists(tu_path))) { + std::thread path_open(LaunchFileExplorer, tu_path); + path_open.detach(); + } + + ImGui::EndPopup(); + } +} + +void TitleListUI::OnDraw(ImGuiIO& io) { + ImGui::SetNextWindowPos(drawing_position_, ImGuiCond_FirstUseEver); + const auto xenia_window_size = ImGui::GetMainViewport()->Size; + + ImGui::SetNextWindowSizeConstraints( + ImVec2(xenia_window_size.x * 0.05f, xenia_window_size.y * 0.05f), + ImVec2(xenia_window_size.x * 0.4f, xenia_window_size.y * 0.5f)); + ImGui::SetNextWindowBgAlpha(0.8f); + + bool dialog_open = true; + if (!ImGui::Begin(dialog_name_.c_str(), &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + Close(); + ImGui::End(); + return; + } + + if (!info_.empty()) { + if (info_.size() > 10) { + ImGui::Text("Search: "); + ImGui::SameLine(); + ImGui::InputText("##Search", title_name_filter_, title_name_filter_size); + ImGui::Separator(); + } + + if (ImGui::BeginTable("", 2, ImGuiTableFlags_BordersInnerH)) { + ImGui::TableNextRow(0, xe::ui::default_image_icon_size.y); + for (auto& entry : info_) { + std::string filter(title_name_filter_); + if (!filter.empty()) { + bool contains_filter = + utf8::lower_ascii(xe::to_utf8(entry.title_name)) + .find(utf8::lower_ascii(filter)) != std::string::npos; + + if (!contains_filter) { + continue; + } + } + DrawTitleEntry(io, entry); + } + ImGui::EndTable(); + } + } else { + // Align text to the center + std::string no_entries_message = "There are no titles, so far."; + + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + float windowWidth = ImGui::GetContentRegionAvail().x; + ImVec2 textSize = ImGui::CalcTextSize(no_entries_message.c_str()); + float textOffsetX = (windowWidth - textSize.x) * 0.5f; + if (textOffsetX > 0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + textOffsetX); + } + + ImGui::Text("%s", no_entries_message.c_str()); + ImGui::PopFont(); + } + + if (!dialog_open) { + Close(); + ImGui::End(); + return; + } + + ImGui::End(); +} + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/ui/title_info_ui.h b/src/xenia/kernel/xam/ui/title_info_ui.h new file mode 100644 index 000000000..7b3af0eac --- /dev/null +++ b/src/xenia/kernel/xam/ui/title_info_ui.h @@ -0,0 +1,53 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_UI_TITLE_INFO_UI_H_ +#define XENIA_KERNEL_XAM_UI_TITLE_INFO_UI_H_ + +#include "xenia/kernel/xam/xam_ui.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace ui { + +class TitleListUI final : public XamDialog { + public: + TitleListUI(xe::ui::ImGuiDrawer* imgui_drawer, const ImVec2 drawing_position, + const UserProfile* profile); + + private: + ~TitleListUI(); + + void OnDraw(ImGuiIO& io) override; + + void LoadProfileTitleList(xe::ui::ImGuiDrawer* imgui_drawer, + const UserProfile* profile); + void DrawTitleEntry(ImGuiIO& io, TitleInfo& entry); + + static constexpr uint8_t title_name_filter_size = 15; + + std::string dialog_name_ = ""; + char title_name_filter_[title_name_filter_size] = ""; + uint32_t selected_title_ = 0; + const ImVec2 drawing_position_ = {}; + + const UserProfile* profile_; + const ProfileManager* profile_manager_; + + std::map> title_icon; + std::vector info_; +}; + +} // namespace ui +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/user_data.h b/src/xenia/kernel/xam/user_data.h index 3d2284d8f..7b5a96919 100644 --- a/src/xenia/kernel/xam/user_data.h +++ b/src/xenia/kernel/xam/user_data.h @@ -84,6 +84,29 @@ class UserData { return {extended_data_.data(), extended_data_.size()}; } + UserDataTypes get_host_data() const { + if (data_.type == X_USER_DATA_TYPE::INT32) { + return data_.data.s32; + } + + if (data_.type == X_USER_DATA_TYPE::DATETIME) { + return data_.data.s64; + } + + if (data_.type == X_USER_DATA_TYPE::WSTRING) { + if (get_extended_data().empty()) { + return std::u16string(); + } + + const char16_t* str_begin = + reinterpret_cast(get_extended_data().data()); + + return string_util::read_u16string_and_swap(str_begin); + } + + return 0; + } + bool is_valid_type() const { return data_.type >= X_USER_DATA_TYPE::CONTEXT && data_.type <= X_USER_DATA_TYPE::DATETIME; diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 64a0eb0a1..c70e41193 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -93,7 +93,32 @@ void UserProfile::LoadProfileIcon(XTileType tile_type) { &written_bytes); file->Destroy(); - profile_images_.insert({tile_type, data}); + profile_images_.insert_or_assign(tile_type, data); +} + +void UserProfile::WriteProfileIcon(XTileType tile_type, + std::span icon_data) { + const std::string path = + fmt::format("User_{:016X}:\\{}", xuid_, kTileFileNames.at(tile_type)); + + vfs::File* file = nullptr; + vfs::FileAction action; + + const X_STATUS result = kernel_state()->file_system()->OpenFile( + nullptr, path, vfs::FileDisposition::kOverwriteIf, + vfs::FileAccess::kGenericAll, false, true, &file, &action); + + if (result != X_STATUS_SUCCESS) { + return; + } + + size_t written_bytes = 0; + + file->WriteSync({icon_data.data(), icon_data.size()}, 0, &written_bytes); + file->Destroy(); + + profile_images_.insert_or_assign( + tile_type, std::vector(icon_data.begin(), icon_data.end())); } std::vector UserProfile::LoadGpd(const uint32_t title_id) { diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 771ab1781..0ece0c6db 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -46,6 +46,14 @@ struct X_USER_PROFILE_SETTING { }; static_assert_size(X_USER_PROFILE_SETTING, 40); +enum class X_USER_PROFILE_GAMERCARD_ZONE_OPTIONS { + GAMERCARD_ZONE_NONE, + GAMERCARD_ZONE_RR, + GAMERCARD_ZONE_PRO, + GAMERCARD_ZONE_FAMILY, + GAMERCARD_ZONE_UNDERGROUND +}; + enum class XTileType { kAchievement, kGameIcon, @@ -75,6 +83,9 @@ static const std::map kTileFileNames = { {XTileType::kAvatarGamerTileSmall, "avtr_32.png"}, }; +static constexpr std::pair kProfileIconSize = {64, 64}; +static constexpr std::pair kProfileIconSizeSmall = {32, 32}; + class UserProfile { public: UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info); @@ -88,6 +99,7 @@ class UserProfile { uint32_t GetSubscriptionTier() const { return account_info_.GetSubscriptionTier(); } + bool IsLiveEnabled() const { return account_info_.IsLiveEnabled(); } std::span GetProfileIcon(XTileType icon_type) { // Overwrite same types? @@ -132,6 +144,8 @@ class UserProfile { void LoadProfileGpds(); void LoadProfileIcon(XTileType tile_type); + void WriteProfileIcon(XTileType tile_type, + std::span icon_data); std::vector LoadGpd(const uint32_t title_id); bool WriteGpd(const uint32_t title_id); }; diff --git a/src/xenia/kernel/xam/user_settings.h b/src/xenia/kernel/xam/user_settings.h index 6b0f59ba7..8c066b343 100644 --- a/src/xenia/kernel/xam/user_settings.h +++ b/src/xenia/kernel/xam/user_settings.h @@ -379,7 +379,7 @@ const static std::set title_writable_settings = { UserSettingId::XPROFILE_TITLE_SPECIFIC2, UserSettingId::XPROFILE_TITLE_SPECIFIC3}; -enum PREFERRED_COLOR_OPTIONS : uint32_t { +enum PREFERRED_COLOR_OPTIONS : uint8_t { PREFERRED_COLOR_NONE, PREFERRED_COLOR_BLACK, PREFERRED_COLOR_WHITE, @@ -393,6 +393,57 @@ enum PREFERRED_COLOR_OPTIONS : uint32_t { PREFERRED_COLOR_BROWN, PREFERRED_COLOR_SILVER }; +enum CONTROLLER_VIBRATION_OPTIONS : uint8_t { + CONTROLLER_VIBRATION_OFF = 0, + CONTROLLER_VIBRATION_ON = 3 +}; + +enum CONTROL_SENSITIVITY_OPTIONS : uint8_t { + CONTROL_SENSITIVITY_MEDIUM = 0, + CONTROL_SENSITIVITY_LOW, + CONTROL_SENSITIVITY_HIGH +}; + +enum GAMER_DIFFICULTY_OPTIONS : uint8_t { + GAMER_DIFFICULTY_NORMAL = 0, + GAMER_DIFFICULTY_EASY, + GAMER_DIFFICULTY_HARD +}; + +enum AUTO_AIM_OPTIONS : uint8_t { AUTO_AIM_OFF = 0, AUTO_AIM_ON }; + +enum AUTO_CENTER_OPTIONS : uint8_t { AUTO_CENTER_OFF = 0, AUTO_CENTER_ON }; + +enum MOVEMENT_CONTROL_OPTIONS : uint8_t { + MOVEMENT_CONTROL_L_THUMBSTICK = 0, + MOVEMENT_CONTROL_R_THUMBSTICK +}; + +enum YAXIS_INVERSION_OPTIONS : uint8_t { + YAXIS_INVERSION_OFF = 0, + YAXIS_INVERSION_ON +}; + +enum TRANSMISSION_OPTIONS : uint8_t { + TRANSMISSION_AUTO = 0, + TRANSMISSION_MANUAL +}; + +enum CAMERA_LOCATION_OPTIONS : uint8_t { + CAMERA_LOCATION_BEHIND = 0, + CAMERA_LOCATION_IN_FRONT, + CAMERA_LOCATION_INSIDE +}; + +enum BRAKE_CONTROL_OPTIONS : uint8_t { + BRAKE_CONTROL_TRIGGER = 0, + BRAKE_CONTROL_BUTTON +}; + +enum ACCELERATOR_CONTROL_OPTIONS : uint8_t { + ACCELERATOR_CONTROL_TRIGGER = 0, + ACCELERATOR_CONTROL_BUTTON +}; class UserSetting : public UserData { public: diff --git a/src/xenia/kernel/xam/user_tracker.cc b/src/xenia/kernel/xam/user_tracker.cc index 05aa98f3f..1c8aab928 100644 --- a/src/xenia/kernel/xam/user_tracker.cc +++ b/src/xenia/kernel/xam/user_tracker.cc @@ -14,6 +14,7 @@ #include #include "third_party/fmt/include/fmt/format.h" +#include "third_party/stb/stb_image.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/user_data.h" @@ -158,6 +159,10 @@ void UserTracker::AddTitleToPlayedList(uint64_t xuid) { return; } + if (!spa_data_->include_in_profile() || spa_data_->is_system_app()) { + return; + } + const uint32_t title_id = spa_data_->title_id(); auto title_gpd = user->games_gpd_.find(title_id); if (title_gpd == user->games_gpd_.end()) { @@ -733,6 +738,35 @@ void UserTracker::UpsertSetting(uint64_t xuid, uint32_t title_id, FlushUserData(xuid); } +bool UserTracker::UpdateUserIcon(uint64_t xuid, + std::span icon_data) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + int width, height, channels; + if (!stbi_info_from_memory(icon_data.data(), + static_cast(icon_data.size()), &width, + &height, &channels)) { + return false; + } + + XTileType icon_type = XTileType::kGameIcon; + + if (std::pair(width, height) == kProfileIconSize) { + icon_type = XTileType::kGamerTile; + } else if (std::pair(width, height) == + kProfileIconSizeSmall) { + icon_type = XTileType::kGamerTileSmall; + } else { + return false; + } + + user->WriteProfileIcon(icon_type, icon_data); + return true; +} + std::span UserTracker::GetIcon(uint64_t xuid, uint32_t title_id, XTileType tile_type, uint64_t tile_id) const { diff --git a/src/xenia/kernel/xam/user_tracker.h b/src/xenia/kernel/xam/user_tracker.h index 943cc81b3..ab39ae122 100644 --- a/src/xenia/kernel/xam/user_tracker.h +++ b/src/xenia/kernel/xam/user_tracker.h @@ -75,6 +75,9 @@ class UserTracker { void UpsertSetting(uint64_t xuid, uint32_t title_id, const UserSetting* setting); + std::optional GetSetting(UserProfile* user, uint32_t title_id, + uint32_t setting_id) const; + bool GetUserSetting(uint64_t xuid, uint32_t title_id, uint32_t setting_id, X_USER_PROFILE_SETTING* setting_ptr, uint32_t& extended_data_address) const; @@ -92,6 +95,8 @@ class UserTracker { uint32_t achievement_id) const; // Images + bool UpdateUserIcon(uint64_t xuid, std::span icon_data); + std::span GetIcon(uint64_t xuid, uint32_t title_id, XTileType tile_type, uint64_t tile_id) const; @@ -100,9 +105,6 @@ class UserTracker { void UpdateSettingValue(uint64_t xuid, uint32_t title_id, UserSettingId setting_id, int32_t difference); - - std::optional GetSetting(UserProfile* user, uint32_t title_id, - uint32_t setting_id) const; std::optional GetGpdSetting(UserProfile* user, uint32_t title_id, uint32_t setting_id) const; diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index 1fd79fca4..71e1bf550 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -7,27 +7,26 @@ ****************************************************************************** */ -#include "third_party/imgui/imgui.h" -#include "xenia/app/profile_dialogs.h" -#include "xenia/base/logging.h" -#include "xenia/base/string_util.h" +#include "xenia/kernel/xam/xam_ui.h" +#include "xenia/app/emulator_window.h" +#include "xenia/base/png_utils.h" #include "xenia/base/system.h" -#include "xenia/emulator.h" #include "xenia/hid/input_system.h" -#include "xenia/kernel/kernel_flags.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" -#include "xenia/kernel/xam/user_tracker.h" #include "xenia/kernel/xam/xam_content_device.h" #include "xenia/kernel/xam/xam_private.h" +#include "xenia/ui/file_picker.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/imgui_guest_notification.h" -#include "xenia/ui/window.h" -#include "xenia/ui/windowed_app_context.h" -#include "xenia/xbox.h" -#include "third_party/fmt/include/fmt/chrono.h" +#include "xenia/kernel/xam/ui/create_profile_ui.h" +#include "xenia/kernel/xam/ui/game_achievements_ui.h" +#include "xenia/kernel/xam/ui/gamercard_ui.h" +#include "xenia/kernel/xam/ui/passcode_ui.h" +#include "xenia/kernel/xam/ui/signin_ui.h" +#include "xenia/kernel/xam/ui/title_info_ui.h" DEFINE_bool(storage_selection_dialog, false, "Show storage device selection dialog when the game requests it.", @@ -58,27 +57,6 @@ namespace xam { extern std::atomic xam_dialogs_shown_; -class XamDialog : public xe::ui::ImGuiDialog { - public: - void set_close_callback(std::function close_callback) { - close_callback_ = close_callback; - } - - protected: - XamDialog(xe::ui::ImGuiDrawer* imgui_drawer) - : xe::ui::ImGuiDialog(imgui_drawer) {} - - virtual ~XamDialog() {} - void OnClose() override { - if (close_callback_) { - close_callback_(); - } - } - - private: - std::function close_callback_ = nullptr; -}; - template X_RESULT xeXamDispatchDialog(T* dialog, std::function close_callback, @@ -257,695 +235,93 @@ X_RESULT xeXamDispatchHeadlessAsync(std::function run_callback) { return X_ERROR_SUCCESS; } -dword_result_t XamIsUIActive_entry() { return xeXamIsUIActive(); } -DECLARE_XAM_EXPORT2(XamIsUIActive, kUI, kImplemented, kHighFrequency); - -class MessageBoxDialog : public XamDialog { - public: - MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, - std::string& description, std::vector buttons, - uint32_t default_button) - : XamDialog(imgui_drawer), - title_(title), - description_(description), - buttons_(std::move(buttons)), - default_button_(default_button), - chosen_button_(default_button) { - if (!title_.size()) { - title_ = "Message Box"; - } +void MessageBoxDialog::OnDraw(ImGuiIO& io) { + bool first_draw = false; + if (!has_opened_) { + ImGui::OpenPopup(title_.c_str()); + has_opened_ = true; + first_draw = true; } - - uint32_t chosen_button() const { return chosen_button_; } - - void OnDraw(ImGuiIO& io) override { - bool first_draw = false; - if (!has_opened_) { - ImGui::OpenPopup(title_.c_str()); - has_opened_ = true; - first_draw = true; + if (ImGui::BeginPopupModal(title_.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + if (description_.size()) { + ImGui::Text("%s", description_.c_str()); } - if (ImGui::BeginPopupModal(title_.c_str(), nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (description_.size()) { - ImGui::Text("%s", description_.c_str()); - } - if (first_draw) { - ImGui::SetKeyboardFocusHere(); - } - for (size_t i = 0; i < buttons_.size(); ++i) { - if (ImGui::Button(buttons_[i].c_str())) { - chosen_button_ = static_cast(i); - ImGui::CloseCurrentPopup(); - Close(); - } - ImGui::SameLine(); - } - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::EndPopup(); - } else { - Close(); + if (first_draw) { + ImGui::SetKeyboardFocusHere(); } - } - virtual ~MessageBoxDialog() {} - - private: - bool has_opened_ = false; - std::string title_; - std::string description_; - std::vector buttons_; - uint32_t default_button_ = 0; - uint32_t chosen_button_ = 0; -}; - -class ProfilePasscodeDialog : public XamDialog { - public: - const char* labelled_keys_[11] = {"None", "X", "Y", "RB", "LB", "LT", - "RT", "Up", "Down", "Left", "Right"}; - - const std::map keys_map_ = { - {"None", 0}, - {"X", X_BUTTON_PASSCODE}, - {"Y", Y_BUTTON_PASSCODE}, - {"RB", RIGHT_BUMPER_PASSCODE}, - {"LB", LEFT_BUMPER_PASSCODE}, - {"LT", LEFT_TRIGGER_PASSCODE}, - {"RT", RIGHT_TRIGGER_PASSCODE}, - {"Up", DPAD_UP_PASSCODE}, - {"Down", DPAD_DOWN_PASSCODE}, - {"Left", DPAD_LEFT_PASSCODE}, - {"Right", DPAD_RIGHT_PASSCODE}}; - - ProfilePasscodeDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, - std::string& description, MESSAGEBOX_RESULT* result_ptr) - : XamDialog(imgui_drawer), - title_(title), - description_(description), - result_ptr_(result_ptr) { - std::memset(result_ptr, 0, sizeof(MESSAGEBOX_RESULT)); - - if (title_.empty()) { - title_ = "Enter Pass Code"; - } - - if (description_.empty()) { - description_ = "Enter your Xbox LIVE pass code."; - } - } - - void DrawPasscodeField(uint8_t key_id) { - const std::string label = fmt::format("##Key {}", key_id); - - if (ImGui::BeginCombo(label.c_str(), - labelled_keys_[key_indexes_[key_id]])) { - for (uint8_t key_index = 0; key_index < keys_map_.size(); key_index++) { - bool is_selected = key_id == key_index; - - if (ImGui::Selectable(labelled_keys_[key_index], is_selected)) { - key_indexes_[key_id] = key_index; - } - - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - - ImGui::EndCombo(); - } - } - - void OnDraw(ImGuiIO& io) override { - if (!has_opened_) { - ImGui::OpenPopup(title_.c_str()); - has_opened_ = true; - } - - if (ImGui::BeginPopupModal(title_.c_str(), nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (description_.size()) { - ImGui::Text("%s", description_.c_str()); - } - - for (uint8_t i = 0; i < passcode_length; i++) { - DrawPasscodeField(i); - // result_ptr_->Passcode[i] = - // keys_map_.at(labelled_keys_[key_indexes_[i]]); - } - - ImGui::NewLine(); - - // We write each key on close to prevent simultaneous dialogs. - if (ImGui::Button("Sign In")) { - for (uint8_t i = 0; i < passcode_length; i++) { - result_ptr_->Passcode[i] = - keys_map_.at(labelled_keys_[key_indexes_[i]]); - } - - selected_signed_in_ = true; - + for (size_t i = 0; i < buttons_.size(); ++i) { + if (ImGui::Button(buttons_[i].c_str())) { + chosen_button_ = static_cast(i); + ImGui::CloseCurrentPopup(); Close(); } - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - Close(); - } } - + ImGui::Spacing(); + ImGui::Spacing(); ImGui::EndPopup(); + } else { + Close(); } +} - virtual ~ProfilePasscodeDialog() {} - - bool SelectedSignedIn() const { return selected_signed_in_; } - - private: - bool has_opened_ = false; - bool selected_signed_in_ = false; - std::string title_; - std::string description_; - - static const uint8_t passcode_length = sizeof(X_XAMACCOUNTINFO::passcode); - int key_indexes_[passcode_length] = {0, 0, 0, 0}; - MESSAGEBOX_RESULT* result_ptr_; -}; - -class GamertagModifyDialog final : public ui::ImGuiDialog { - public: - GamertagModifyDialog(ui::ImGuiDrawer* imgui_drawer, - ProfileManager* profile_manager, uint64_t xuid) - : ui::ImGuiDialog(imgui_drawer), - profile_manager_(profile_manager), - xuid_(xuid) { - memset(gamertag_, 0, sizeof(gamertag_)); +void KeyboardInputDialog::OnDraw(ImGuiIO& io) { + bool first_draw = false; + if (!has_opened_) { + ImGui::OpenPopup(title_.c_str()); + has_opened_ = true; + first_draw = true; } - - private: - void OnDraw(ImGuiIO& io) override { - if (!has_opened_) { - ImGui::OpenPopup("Modify Gamertag"); - has_opened_ = true; + if (ImGui::BeginPopupModal(title_.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + if (description_.size()) { + ImGui::TextWrapped("%s", description_.c_str()); } - - bool dialog_open = true; - if (!ImGui::BeginPopupModal("Modify Gamertag", &dialog_open, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - Close(); - return; + if (first_draw) { + ImGui::SetKeyboardFocusHere(); } - - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - !ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0)) { - ImGui::SetKeyboardFocusHere(0); + ImGui::PushID("input_text"); + bool input_submitted = + ImGui::InputText("##body", text_buffer_.data(), text_buffer_.size(), + ImGuiInputTextFlags_EnterReturnsTrue); + // Context menu for paste functionality + if (ImGui::BeginPopupContextItem("input_context_menu")) { + if (ImGui::MenuItem("Paste")) { + if (ImGui::GetClipboardText() != nullptr) { + std::string clipboard_text = ImGui::GetClipboardText(); + xe::string_util::copy_truncating(text_buffer_.data(), clipboard_text, + text_buffer_.size()); + } + } + ImGui::EndPopup(); } - - ImGui::TextUnformatted("New gamertag:"); - ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); - - const std::string gamertag_string = std::string(gamertag_); - bool valid = profile_manager_->IsGamertagValid(gamertag_string); - - ImGui::BeginDisabled(!valid); - if (ImGui::Button("Update")) { - profile_manager_->ModifyGamertag(xuid_, gamertag_string); - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - ImGui::EndDisabled(); - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - - if (!dialog_open) { + ImGui::PopID(); + if (input_submitted) { + text_ = std::string(text_buffer_.data(), text_buffer_.size()); + cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); - ImGui::EndPopup(); - return; } + if (ImGui::Button("OK")) { + text_ = std::string(text_buffer_.data(), text_buffer_.size()); + cancelled_ = false; + ImGui::CloseCurrentPopup(); + Close(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + text_ = ""; + cancelled_ = true; + ImGui::CloseCurrentPopup(); + Close(); + } + ImGui::Spacing(); ImGui::EndPopup(); - }; - - bool has_opened_ = false; - char gamertag_[16] = ""; - const uint64_t xuid_; - ProfileManager* profile_manager_; -}; - -class GameAchievementsDialog final : public XamDialog { - public: - GameAchievementsDialog(ui::ImGuiDrawer* imgui_drawer, - const ImVec2 drawing_position, - const TitleInfo* title_info, - const UserProfile* profile) - : XamDialog(imgui_drawer), - drawing_position_(drawing_position), - title_info_(*title_info), - profile_(profile), - window_id_(GetWindowId()) { - LoadAchievementsData(); + } else { + Close(); } - - private: - ~GameAchievementsDialog() { - for (auto& entry : achievements_icons_) { - entry.second.release(); - } - } - bool LoadAchievementsData() { - xe::ui::IconsData data; - - achievements_info_ = - kernel_state() - ->xam_state() - ->achievement_manager() - ->GetTitleAchievements(profile_->xuid(), title_info_.id); - - if (achievements_info_.empty()) { - return false; - } - - for (const Achievement& entry : achievements_info_) { - const auto icon = - kernel_state() - ->xam_state() - ->achievement_manager() - ->GetAchievementIcon(profile_->xuid(), title_info_.id, - entry.achievement_id); - - data.insert({entry.image_id, icon}); - } - - achievements_icons_ = imgui_drawer()->LoadIcons(data); - return true; - } - - std::string GetAchievementTitle(const Achievement& achievement_entry) const { - std::string title = "Secret trophy"; - - if (achievement_entry.IsUnlocked() || show_locked_info_ || - achievement_entry.flags & - static_cast(AchievementFlags::kShowUnachieved)) { - title = xe::to_utf8(achievement_entry.achievement_name); - } - - return title; - } - - std::string GetAchievementDescription( - const Achievement& achievement_entry) const { - std::string description = "Hidden description"; - - if (achievement_entry.flags & - static_cast(AchievementFlags::kShowUnachieved)) { - description = xe::to_utf8(achievement_entry.locked_description); - } - - if (achievement_entry.IsUnlocked() || show_locked_info_) { - description = xe::to_utf8(achievement_entry.unlocked_description); - } - - return description; - } - - ui::ImmediateTexture* GetIcon(const Achievement& achievement_entry) const { - if (!achievement_entry.IsUnlocked() && !show_locked_info_) { - return imgui_drawer()->GetLockedAchievementIcon(); - } - - if (achievements_icons_.count(achievement_entry.image_id)) { - return achievements_icons_.at(achievement_entry.image_id).get(); - } - - if (achievement_entry.IsUnlocked()) { - return nullptr; - } - return imgui_drawer()->GetLockedAchievementIcon(); - } - - std::string GetUnlockedTime(const Achievement& achievement_entry) const { - if (achievement_entry.IsUnlockedOnline()) { - const auto unlock_time = chrono::WinSystemClock::to_local( - achievement_entry.unlock_time.to_time_point()); - - return fmt::format("Unlocked: {:%Y-%m-%d %H:%M}", - std::chrono::system_clock::time_point( - unlock_time.time_since_epoch())); - } - - if (achievement_entry.unlock_time.is_valid()) { - const auto unlock_time = chrono::WinSystemClock::to_local( - achievement_entry.unlock_time.to_time_point()); - - return fmt::format("Unlocked: Offline ({:%Y-%m-%d %H:%M})", - std::chrono::system_clock::time_point( - unlock_time.time_since_epoch())); - } - return fmt::format("Unlocked: Offline"); - } - - void DrawTitleAchievementInfo(ImGuiIO& io, - const Achievement& achievement_entry) const { - const auto start_drawing_pos = ImGui::GetCursorPos(); - - ImGui::TableSetColumnIndex(0); - - const auto icon = GetIcon(achievement_entry); - if (icon) { - ImGui::Image(reinterpret_cast(GetIcon(achievement_entry)), - ui::default_image_icon_size); - } else { - ImGui::Dummy(ui::default_image_icon_size); - } - ImGui::TableNextColumn(); - - ImGui::PushFont(imgui_drawer()->GetTitleFont()); - const auto primary_line_height = ImGui::GetTextLineHeight(); - ImGui::Text("%s", GetAchievementTitle(achievement_entry).c_str()); - ImGui::PopFont(); - - ImGui::PushTextWrapPos(ImGui::GetMainViewport()->Size.x * 0.5f); - ImGui::TextWrapped("%s", - GetAchievementDescription(achievement_entry).c_str()); - ImGui::PopTextWrapPos(); - - ImGui::SetCursorPosY(start_drawing_pos.y + ui::default_image_icon_size.x - - ImGui::GetTextLineHeight()); - - if (achievement_entry.IsUnlocked()) { - ImGui::Text("%s", GetUnlockedTime(achievement_entry).c_str()); - } - - ImGui::TableNextColumn(); - - // TODO(Gliniak): There is no easy way to align text to middle, so I have to - // do it manually. - const float achievement_row_middle_alignment = - ((ui::default_image_icon_size.x / 2.f) - - ImGui::GetTextLineHeight() / 2.f) * - 0.85f; - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + - achievement_row_middle_alignment); - ImGui::PushFont(imgui_drawer()->GetTitleFont()); - ImGui::TextUnformatted( - fmt::format("{} G", achievement_entry.gamerscore).c_str()); - ImGui::PopFont(); - } - - void OnDraw(ImGuiIO& io) override { - ImGui::SetNextWindowPos(drawing_position_, ImGuiCond_FirstUseEver); - - const auto xenia_window_size = ImGui::GetMainViewport()->Size; - - ImGui::SetNextWindowSizeConstraints( - ImVec2(xenia_window_size.x * 0.2f, xenia_window_size.y * 0.3f), - ImVec2(xenia_window_size.x * 0.6f, xenia_window_size.y * 0.8f)); - ImGui::SetNextWindowBgAlpha(0.8f); - - bool dialog_open = true; - - std::string title_name = xe::to_utf8(title_info_.title_name); - title_name.erase(std::remove(title_name.begin(), title_name.end(), '\0'), - title_name.end()); - - const std::string window_name = - fmt::format("{} Achievements###{}", title_name, window_id_); - if (!ImGui::Begin(window_name.c_str(), &dialog_open, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - Close(); - ImGui::End(); - return; - } - - ImGui::Checkbox("Show locked achievements information", &show_locked_info_); - ImGui::Separator(); - - if (achievements_info_.empty()) { - ImGui::TextUnformatted(fmt::format("No achievements data!").c_str()); - } else { - if (ImGui::BeginTable("", 3, - ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) { - for (const auto& entry : achievements_info_) { - ImGui::TableNextRow(0, ui::default_image_icon_size.y); - DrawTitleAchievementInfo(io, entry); - } - - ImGui::EndTable(); - } - } - - if (!dialog_open) { - Close(); - ImGui::End(); - return; - } - - ImGui::End(); - }; - - bool show_locked_info_ = false; - - uint64_t window_id_; - const ImVec2 drawing_position_ = {}; - - const TitleInfo title_info_; - const UserProfile* profile_; - - std::vector achievements_info_; - std::map> achievements_icons_; -}; - -class GamesInfoDialog final : public XamDialog { - public: - GamesInfoDialog(ui::ImGuiDrawer* imgui_drawer, const ImVec2 drawing_position, - const UserProfile* profile) - : XamDialog(imgui_drawer), - drawing_position_(drawing_position), - profile_(profile), - profile_manager_(kernel_state()->xam_state()->profile_manager()), - dialog_name_(fmt::format("{}'s Games List###{}", profile->name(), - GetWindowId())) { - LoadProfileGameInfo(imgui_drawer, profile); - } - - private: - ~GamesInfoDialog() { - for (auto& entry : title_icon) { - entry.second.release(); - } - } - void LoadProfileGameInfo(ui::ImGuiDrawer* imgui_drawer, - const UserProfile* profile) { - info_.clear(); - - xe::ui::IconsData data; - - info_ = kernel_state()->xam_state()->user_tracker()->GetPlayedTitles( - profile->xuid()); - for (const auto& title_info : info_) { - if (!title_info.icon.empty()) { - data[title_info.id] = title_info.icon; - } - } - - title_icon = imgui_drawer->LoadIcons(data); - } - - void DrawTitleEntry(ImGuiIO& io, TitleInfo& entry) { - const auto start_position = ImGui::GetCursorPos(); - const ImVec2 next_window_position = - ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f, - ImGui::GetWindowPos().y); - - // First Column - ImGui::TableSetColumnIndex(0); - - if (title_icon.count(entry.id)) { - ImGui::Image(reinterpret_cast(title_icon.at(entry.id).get()), - ui::default_image_icon_size); - } else { - ImGui::Dummy(ui::default_image_icon_size); - } - - // Second Column - ImGui::TableNextColumn(); - ImGui::PushFont(imgui_drawer()->GetTitleFont()); - ImGui::TextUnformatted(xe::to_utf8(entry.title_name).c_str()); - ImGui::PopFont(); - - ImGui::TextUnformatted( - fmt::format("{}/{} Achievements unlocked ({} Gamerscore)", - entry.unlocked_achievements_count, entry.achievements_count, - entry.title_earned_gamerscore) - .c_str()); - - ImGui::SetCursorPosY(start_position.y + ui::default_image_icon_size.y - - ImGui::GetTextLineHeight()); - - if (entry.WasTitlePlayed()) { - ImGui::TextUnformatted( - fmt::format("Last played: {:%Y-%m-%d %H:%M}", - std::chrono::system_clock::time_point( - entry.last_played.time_since_epoch())) - .c_str()); - } else { - ImGui::TextUnformatted("Last played: Unknown"); - } - ImGui::TableNextColumn(); - - const ImVec2 end_draw_position = - ImVec2(ImGui::GetCursorPos().x - start_position.x, - ImGui::GetCursorPos().y - start_position.y); - - ImGui::SetCursorPos(start_position); - - if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(), - selected_title_ == entry.id, - ImGuiSelectableFlags_SpanAllColumns, - end_draw_position)) { - selected_title_ = entry.id; - new GameAchievementsDialog(imgui_drawer(), next_window_position, &entry, - profile_); - } - - if (ImGui::BeginPopupContextItem( - fmt::format("Title Menu {:08X}", entry.id).c_str())) { - selected_title_ = entry.id; - if (ImGui::MenuItem("Refresh title stats", nullptr, nullptr, true)) { - kernel_state()->xam_state()->user_tracker()->RefreshTitleSummary( - profile_->xuid(), entry.id); - - const auto title_info = - kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo( - profile_->xuid(), entry.id); - - if (title_info) { - entry = title_info.value(); - } - } - - const auto savefile_path = profile_manager_->GetProfileContentPath( - profile_->xuid(), entry.id, XContentType::kSavedGame); - - const auto dlc_path = profile_manager_->GetProfileContentPath( - 0, entry.id, XContentType::kMarketplaceContent); - - const auto tu_path = profile_manager_->GetProfileContentPath( - 0, entry.id, XContentType::kInstaller); - - if (ImGui::MenuItem("Open savefile directory", nullptr, nullptr, - std::filesystem::exists(savefile_path))) { - std::thread path_open(LaunchFileExplorer, savefile_path); - path_open.detach(); - } - if (ImGui::MenuItem("Open DLC directory", nullptr, nullptr, - std::filesystem::exists(dlc_path))) { - std::thread path_open(LaunchFileExplorer, dlc_path); - path_open.detach(); - } - if (ImGui::MenuItem("Open Title Update directory", nullptr, nullptr, - std::filesystem::exists(tu_path))) { - std::thread path_open(LaunchFileExplorer, tu_path); - path_open.detach(); - } - - ImGui::EndPopup(); - } - } - - void OnDraw(ImGuiIO& io) override { - ImGui::SetNextWindowPos(drawing_position_, ImGuiCond_FirstUseEver); - const auto xenia_window_size = ImGui::GetMainViewport()->Size; - - ImGui::SetNextWindowSizeConstraints( - ImVec2(xenia_window_size.x * 0.05f, xenia_window_size.y * 0.05f), - ImVec2(xenia_window_size.x * 0.4f, xenia_window_size.y * 0.5f)); - ImGui::SetNextWindowBgAlpha(0.8f); - - bool dialog_open = true; - if (!ImGui::Begin(dialog_name_.c_str(), &dialog_open, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - Close(); - ImGui::End(); - return; - } - - if (!info_.empty()) { - if (info_.size() > 10) { - ImGui::Text("Search: "); - ImGui::SameLine(); - ImGui::InputText("##Search", title_name_filter_, - title_name_filter_size); - ImGui::Separator(); - } - - if (ImGui::BeginTable("", 2, - ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) { - ImGui::TableNextRow(0, ui::default_image_icon_size.y); - for (auto& entry : info_) { - std::string filter(title_name_filter_); - if (!filter.empty()) { - bool contains_filter = - utf8::lower_ascii(xe::to_utf8(entry.title_name)) - .find(utf8::lower_ascii(filter)) != std::string::npos; - - if (!contains_filter) { - continue; - } - } - DrawTitleEntry(io, entry); - } - ImGui::EndTable(); - } - } else { - // Align text to the center - std::string no_entries_message = "There are no titles, so far."; - - ImGui::PushFont(imgui_drawer()->GetTitleFont()); - float windowWidth = ImGui::GetContentRegionAvail().x; - ImVec2 textSize = ImGui::CalcTextSize(no_entries_message.c_str()); - float textOffsetX = (windowWidth - textSize.x) * 0.5f; - if (textOffsetX > 0.0f) { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + textOffsetX); - } - - ImGui::Text("%s", no_entries_message.c_str()); - ImGui::PopFont(); - } - - if (!dialog_open) { - Close(); - ImGui::End(); - return; - } - - ImGui::End(); - } - - static constexpr uint8_t title_name_filter_size = 15; - - std::string dialog_name_ = ""; - char title_name_filter_[title_name_filter_size] = ""; - uint32_t selected_title_ = 0; - const ImVec2 drawing_position_ = {}; - - const UserProfile* profile_; - const ProfileManager* profile_manager_; - - std::map> title_icon; - std::vector info_; -}; +} static dword_result_t XamShowMessageBoxUi( dword_t user_index, lpu16string_t title_ptr, lpu16string_t text_ptr, @@ -985,11 +361,11 @@ static dword_result_t XamShowMessageBoxUi( } const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); if (flags & XMBox_PASSCODEMODE || flags & XMBox_VERIFYPASSCODEMODE) { auto close = [result_ptr, - active_button](ProfilePasscodeDialog* dialog) -> X_RESULT { + active_button](ui::ProfilePasscodeUI* dialog) -> X_RESULT { if (dialog->SelectedSignedIn()) { // Logged in return X_ERROR_SUCCESS; @@ -998,8 +374,8 @@ static dword_result_t XamShowMessageBoxUi( } }; - result = xeXamDispatchDialog( - new ProfilePasscodeDialog(imgui_drawer, title, text, result_ptr), + result = xeXamDispatchDialog( + new ui::ProfilePasscodeUI(imgui_drawer, title, text, result_ptr), close, overlapped); } else { auto close = [result_ptr](MessageBoxDialog* dialog) -> X_RESULT { @@ -1017,6 +393,9 @@ static dword_result_t XamShowMessageBoxUi( return result; } +dword_result_t XamIsUIActive_entry() { return xeXamIsUIActive(); } +DECLARE_XAM_EXPORT2(XamIsUIActive, kUI, kImplemented, kHighFrequency); + // https://www.se7ensins.com/forums/threads/working-xshowmessageboxui.844116/ dword_result_t XamShowMessageBoxUI_entry( dword_t user_index, lpu16string_t title_ptr, lpu16string_t text_ptr, @@ -1055,7 +434,7 @@ dword_result_t XNotifyQueueUI_entry(dword_t exnq, dword_t dwUserIndex, XELOGI("XNotifyQueueUI: {}", displayText); const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); new xe::ui::XNotifyWindow(imgui_drawer, "", displayText, dwUserIndex, position_id); @@ -1066,103 +445,6 @@ dword_result_t XNotifyQueueUI_entry(dword_t exnq, dword_t dwUserIndex, } DECLARE_XAM_EXPORT1(XNotifyQueueUI, kUI, kSketchy); -class KeyboardInputDialog : public XamDialog { - public: - KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, - std::string& description, std::string& default_text, - size_t max_length) - : XamDialog(imgui_drawer), - title_(title), - description_(description), - default_text_(default_text), - max_length_(max_length), - text_buffer_() { - if (!title_.size()) { - if (!description_.size()) { - title_ = "Keyboard Input"; - } else { - title_ = description_; - description_ = ""; - } - } - text_ = default_text; - text_buffer_.resize(max_length); - xe::string_util::copy_truncating(text_buffer_.data(), default_text_, - text_buffer_.size()); - } - virtual ~KeyboardInputDialog() {} - - const std::string& text() const { return text_; } - bool cancelled() const { return cancelled_; } - - void OnDraw(ImGuiIO& io) override { - bool first_draw = false; - if (!has_opened_) { - ImGui::OpenPopup(title_.c_str()); - has_opened_ = true; - first_draw = true; - } - if (ImGui::BeginPopupModal(title_.c_str(), nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (description_.size()) { - ImGui::TextWrapped("%s", description_.c_str()); - } - if (first_draw) { - ImGui::SetKeyboardFocusHere(); - } - ImGui::PushID("input_text"); - bool input_submitted = - ImGui::InputText("##body", text_buffer_.data(), text_buffer_.size(), - ImGuiInputTextFlags_EnterReturnsTrue); - // Context menu for paste functionality - if (ImGui::BeginPopupContextItem("input_context_menu")) { - if (ImGui::MenuItem("Paste")) { - if (ImGui::GetClipboardText() != nullptr) { - std::string clipboard_text = ImGui::GetClipboardText(); - xe::string_util::copy_truncating( - text_buffer_.data(), clipboard_text, text_buffer_.size()); - } - } - ImGui::EndPopup(); - } - ImGui::PopID(); - if (input_submitted) { - text_ = std::string(text_buffer_.data(), text_buffer_.size()); - cancelled_ = false; - ImGui::CloseCurrentPopup(); - Close(); - } - if (ImGui::Button("OK")) { - text_ = std::string(text_buffer_.data(), text_buffer_.size()); - cancelled_ = false; - ImGui::CloseCurrentPopup(); - Close(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - text_ = ""; - cancelled_ = true; - ImGui::CloseCurrentPopup(); - Close(); - } - ImGui::Spacing(); - ImGui::EndPopup(); - } else { - Close(); - } - } - - private: - bool has_opened_ = false; - std::string title_; - std::string description_; - std::string default_text_; - size_t max_length_ = 0; - std::vector text_buffer_; - std::string text_ = ""; - bool cancelled_ = true; -}; - // https://www.se7ensins.com/forums/threads/release-how-to-use-xshowkeyboardui-release.906568/ dword_result_t XamShowKeyboardUI_entry( dword_t user_index, dword_t flags, lpu16string_t default_text, @@ -1208,7 +490,7 @@ dword_result_t XamShowKeyboardUI_entry( } }; const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); std::string title_str = title ? xe::to_utf8(title.value()) : ""; std::string desc_str = description ? xe::to_utf8(description.value()) : ""; @@ -1279,7 +561,7 @@ dword_result_t XamShowDeviceSelectorUI_entry( buttons.push_back("Cancel"); const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, overlapped); @@ -1299,7 +581,7 @@ void XamShowDirtyDiscErrorUI_entry(dword_t user_index) { "likely caused by bad or unimplemented file IO calls."; const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, {"OK"}, 0), [](MessageBoxDialog*) -> X_RESULT { return X_ERROR_SUCCESS; }, 0); @@ -1456,7 +738,7 @@ X_HRESULT xeXShowMarketplaceUIEx(dword_t user_index, dword_t ui_type, } const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialogAsync( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close); } @@ -1546,7 +828,7 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry( "be installed manually using File -> Install Content."; const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, overlapped); @@ -1559,463 +841,68 @@ dword_result_t XamShowForcedNameChangeUI_entry(dword_t user_index) { } DECLARE_XAM_EXPORT1(XamShowForcedNameChangeUI, kUI, kImplemented); -bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid, - const uint8_t user_index, +bool xeDrawProfileContent(xe::ui::ImGuiDrawer* imgui_drawer, + const uint64_t xuid, const uint8_t user_index, const X_XAMACCOUNTINFO* account, + const xe::ui::ImmediateTexture* profile_icon, + std::function context_menu, + std::function on_profile_change, uint64_t* selected_xuid) { - auto profile_manager = kernel_state()->xam_state()->profile_manager(); + const ImVec2 start_position = ImGui::GetCursorPos(); - constexpr float default_image_size = 75.0f; - const ImVec2 next_window_position = - ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f, - ImGui::GetWindowPos().y); - const ImVec2 drawing_start_position = ImGui::GetCursorPos(); - ImVec2 current_drawing_position = ImGui::GetCursorPos(); + ImGui::BeginGroup(); + { + if (profile_icon) { + ImGui::Image(reinterpret_cast(profile_icon), + xe::ui::default_image_icon_size); + } else { + if (user_index < XUserMaxUserCount) { + const auto icon = imgui_drawer->GetNotificationIcon(user_index); + ImGui::Image(reinterpret_cast(icon), + xe::ui::default_image_icon_size); + } else { + ImGui::Dummy(xe::ui::default_image_icon_size); + } + } - // In the future it can be replaced with profile icon. - const auto icon = imgui_drawer->GetNotificationIcon(user_index); - if (icon && user_index < XUserMaxUserCount) { - ImGui::Image(reinterpret_cast(icon), - ui::default_image_icon_size); - } else { - ImGui::Dummy(ui::default_image_icon_size); + ImGui::SameLine(); + + ImGui::BeginGroup(); + { + ImGui::TextUnformatted( + fmt::format("User: {}\n", account->GetGamertagString()).c_str()); + ImGui::TextUnformatted(fmt::format("XUID: {:016X} \n", xuid).c_str()); + if (user_index != XUserIndexAny) { + ImGui::TextUnformatted( + fmt::format("Assigned to slot: {}\n", user_index + 1).c_str()); + } else { + ImGui::TextUnformatted(fmt::format("Profile is not signed in").c_str()); + } + } + ImGui::EndGroup(); } - - ImGui::SameLine(); - current_drawing_position = ImGui::GetCursorPos(); - ImGui::TextUnformatted( - fmt::format("User: {}\n", account->GetGamertagString()).c_str()); - - ImGui::SameLine(); - ImGui::SetCursorPos(current_drawing_position); - ImGui::SetCursorPosY(current_drawing_position.y + ImGui::GetTextLineHeight()); - ImGui::TextUnformatted(fmt::format("XUID: {:016X} \n", xuid).c_str()); - - ImGui::SameLine(); - ImGui::SetCursorPos(current_drawing_position); - ImGui::SetCursorPosY(current_drawing_position.y + - 2 * ImGui::GetTextLineHeight()); - - if (user_index != XUserIndexAny) { - ImGui::TextUnformatted( - fmt::format("Assigned to slot: {}\n", user_index + 1).c_str()); - } else { - ImGui::TextUnformatted(fmt::format("Profile is not signed in").c_str()); - } - - const ImVec2 drawing_end_position = ImGui::GetCursorPos(); + ImGui::EndGroup(); if (xuid && selected_xuid) { - ImGui::SetCursorPos(drawing_start_position); + const ImVec2 end_draw_position = + ImVec2(ImGui::GetCursorPos().x - start_position.x, + ImGui::GetCursorPos().y - start_position.y); - if (ImGui::Selectable( - "##Selectable", *selected_xuid == xuid, - ImGuiSelectableFlags_SpanAllColumns, - ImVec2(drawing_end_position.x - drawing_start_position.x, - drawing_end_position.y - drawing_start_position.y))) { + ImGui::SetCursorPos(start_position); + if (ImGui::Selectable("##Selectable", *selected_xuid == xuid, + ImGuiSelectableFlags_SpanAllColumns, + end_draw_position)) { *selected_xuid = xuid; } - if (ImGui::BeginPopupContextItem("Profile Menu")) { - *selected_xuid = xuid; - if (user_index == XUserIndexAny) { - if (ImGui::MenuItem("Login")) { - profile_manager->Login(xuid); - } - - if (ImGui::BeginMenu("Login to slot:")) { - for (uint8_t i = 1; i <= XUserMaxUserCount; i++) { - if (ImGui::MenuItem(fmt::format("slot {}", i).c_str())) { - profile_manager->Login(xuid, i - 1); - } - } - ImGui::EndMenu(); - } - } else { - if (ImGui::MenuItem("Logout")) { - profile_manager->Logout(user_index); - } - } - - ImGui::BeginDisabled(kernel_state()->emulator()->is_title_open()); - if (ImGui::BeginMenu("Modify")) { - if (ImGui::MenuItem("Gamertag")) { - new GamertagModifyDialog(imgui_drawer, profile_manager, xuid); - } - - ImGui::MenuItem("Profile Icon (Unsupported)"); - ImGui::EndMenu(); - } - ImGui::EndDisabled(); - - const bool is_signedin = profile_manager->GetProfile(xuid) != nullptr; - ImGui::BeginDisabled(!is_signedin); - if (ImGui::MenuItem("Show Played Titles")) { - new GamesInfoDialog(imgui_drawer, next_window_position, - profile_manager->GetProfile(user_index)); - } - ImGui::EndDisabled(); - - if (ImGui::MenuItem("Show Content Directory")) { - const auto path = profile_manager->GetProfileContentPath( - xuid, kernel_state()->title_id()); - - if (!std::filesystem::exists(path)) { - std::filesystem::create_directories(path); - } - - std::thread path_open(LaunchFileExplorer, path); - path_open.detach(); - } - - if (!kernel_state()->emulator()->is_title_open()) { - ImGui::Separator(); - if (ImGui::BeginMenu("Delete Profile")) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted( - fmt::format("You're about to delete profile: {} (XUID: {:016X}). " - "This will remove all data assigned to this profile " - "including savefiles. Are you sure?", - account->GetGamertagString(), xuid) - .c_str()); - ImGui::EndTooltip(); - - if (ImGui::MenuItem("Yes, delete it!")) { - profile_manager->DeleteProfile(xuid); - ImGui::EndMenu(); - ImGui::EndPopup(); - return false; - } - - ImGui::EndMenu(); - } - } - ImGui::EndPopup(); + if (context_menu) { + return context_menu(); } } return true; } -class SigninDialog : public XamDialog { - public: - SigninDialog(xe::ui::ImGuiDrawer* imgui_drawer, uint32_t users_needed) - : XamDialog(imgui_drawer), - users_needed_(users_needed), - title_("Sign In") { - last_user_ = kernel_state()->emulator()->input_system()->GetLastUsedSlot(); - - for (uint8_t slot = 0; slot < XUserMaxUserCount; slot++) { - std::string name = fmt::format("Slot {:d}", slot + 1); - slot_data_.push_back({slot, name}); - } - } - - virtual ~SigninDialog() {} - - void OnDraw(ImGuiIO& io) override { - bool first_draw = false; - if (!has_opened_) { - ImGui::OpenPopup(title_.c_str()); - has_opened_ = true; - first_draw = true; - ReloadProfiles(true); - } - if (ImGui::BeginPopupModal(title_.c_str(), nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - auto profile_manager = kernel_state()->xam_state()->profile_manager(); - - for (uint32_t i = 0; i < users_needed_; i++) { - ImGui::BeginGroup(); - - std::vector combo_items; - int items_count = 0; - int current_item = 0; - - // Fill slot list. - std::vector slots; - slots.push_back(0xFF); - combo_items.push_back("---"); - for (auto& elem : slot_data_) { - // Select the slot or skip it if it's already used. - bool already_taken = false; - for (uint32_t j = 0; j < users_needed_; j++) { - if (chosen_slots_[j] == elem.first) { - if (i == j) { - current_item = static_cast(combo_items.size()); - } else { - already_taken = true; - } - break; - } - } - - if (already_taken) { - continue; - } - - slots.push_back(elem.first); - combo_items.push_back(elem.second.c_str()); - } - items_count = static_cast(combo_items.size()); - - ImGui::BeginDisabled(users_needed_ == 1); - ImGui::Combo(fmt::format("##Slot{:d}", i).c_str(), ¤t_item, - combo_items.data(), items_count); - chosen_slots_[i] = slots[current_item]; - ImGui::EndDisabled(); - ImGui::Spacing(); - - combo_items.clear(); - current_item = 0; - - // Fill profile list. - std::vector xuids; - xuids.push_back(0); - combo_items.push_back("---"); - if (chosen_slots_[i] != 0xFF) { - for (auto& elem : profile_data_) { - // Select the profile or skip it if it's already used. - bool already_taken = false; - for (uint32_t j = 0; j < users_needed_; j++) { - if (chosen_xuids_[j] == elem.first) { - if (i == j) { - current_item = static_cast(combo_items.size()); - } else { - already_taken = true; - } - break; - } - } - - if (already_taken) { - continue; - } - - xuids.push_back(elem.first); - combo_items.push_back(elem.second.c_str()); - } - } - items_count = static_cast(combo_items.size()); - - ImGui::BeginDisabled(chosen_slots_[i] == 0xFF); - ImGui::Combo(fmt::format("##Profile{:d}", i).c_str(), ¤t_item, - combo_items.data(), items_count); - chosen_xuids_[i] = xuids[current_item]; - ImGui::EndDisabled(); - ImGui::Spacing(); - - // Draw profile badge. - uint8_t slot = chosen_slots_[i]; - uint64_t xuid = chosen_xuids_[i]; - const auto account = profile_manager->GetAccount(xuid); - - if (slot == 0xFF || xuid == 0 || !account) { - float ypos = ImGui::GetCursorPosY(); - ImGui::SetCursorPosY(ypos + ImGui::GetTextLineHeight() * 5); - } else { - xeDrawProfileContent(imgui_drawer(), xuid, slot, account, nullptr); - } - - ImGui::EndGroup(); - if (i != (users_needed_ - 1) && (i == 0 || i == 2)) { - ImGui::SameLine(); - } - } - - ImGui::Spacing(); - - if (ImGui::Button("Create Profile")) { - creating_profile_ = true; - ImGui::OpenPopup("Create Profile"); - first_draw = true; - } - ImGui::Spacing(); - - if (creating_profile_) { - if (ImGui::BeginPopupModal("Create Profile", nullptr, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - if (first_draw) { - ImGui::SetKeyboardFocusHere(); - } - - ImGui::TextUnformatted("Gamertag:"); - ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); - - const std::string gamertag_string = gamertag_; - bool valid = profile_manager->IsGamertagValid(gamertag_string); - - ImGui::BeginDisabled(!valid); - if (ImGui::Button("Create")) { - profile_manager->CreateProfile(gamertag_string, false); - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - ImGui::CloseCurrentPopup(); - creating_profile_ = false; - ReloadProfiles(false); - } - ImGui::EndDisabled(); - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - ImGui::CloseCurrentPopup(); - creating_profile_ = false; - } - - ImGui::EndPopup(); - } else { - creating_profile_ = false; - } - } - - if (ImGui::Button("OK")) { - std::map profile_map; - for (uint32_t i = 0; i < users_needed_; i++) { - uint8_t slot = chosen_slots_[i]; - uint64_t xuid = chosen_xuids_[i]; - if (slot != 0xFF && xuid != 0) { - profile_map[slot] = xuid; - } - } - profile_manager->LoginMultiple(profile_map); - - ImGui::CloseCurrentPopup(); - Close(); - } - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - ImGui::CloseCurrentPopup(); - Close(); - } - - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::EndPopup(); - } else { - Close(); - } - } - - void ReloadProfiles(bool first_draw) { - auto profile_manager = kernel_state()->xam_state()->profile_manager(); - auto profiles = profile_manager->GetAccounts(); - - profile_data_.clear(); - for (auto& [xuid, account] : *profiles) { - profile_data_.push_back({xuid, account.GetGamertagString()}); - } - - if (first_draw) { - // If only one user is requested, request last used controller to sign in. - if (users_needed_ == 1) { - chosen_slots_[0] = last_user_; - } else { - for (uint32_t i = 0; i < users_needed_; i++) { - // TODO: Not sure about this, needs testing on real hardware. - chosen_slots_[i] = i; - } - } - - // Default profile selection to profile that is already signed in. - for (auto& elem : profile_data_) { - uint64_t xuid = elem.first; - uint8_t slot = profile_manager->GetUserIndexAssignedToProfile(xuid); - for (uint32_t j = 0; j < users_needed_; j++) { - if (chosen_slots_[j] != XUserIndexAny && slot == chosen_slots_[j]) { - chosen_xuids_[j] = xuid; - } - } - } - } - } - - private: - bool has_opened_ = false; - std::string title_; - uint32_t users_needed_ = 1; - uint32_t last_user_ = 0; - - std::vector> slot_data_; - std::vector> profile_data_; - uint8_t chosen_slots_[XUserMaxUserCount] = {}; - uint64_t chosen_xuids_[XUserMaxUserCount] = {}; - - bool creating_profile_ = false; - char gamertag_[16] = ""; -}; - -class CreateProfileDialog final : public XamDialog { - public: - CreateProfileDialog(ui::ImGuiDrawer* imgui_drawer, Emulator* emulator) - : XamDialog(imgui_drawer), emulator_(emulator) { - memset(gamertag_, 0, sizeof(gamertag_)); - } - - protected: - void OnDraw(ImGuiIO& io) override; - - bool has_opened_ = false; - char gamertag_[16] = ""; - Emulator* emulator_; -}; - -void CreateProfileDialog::OnDraw(ImGuiIO& io) { - if (!has_opened_) { - ImGui::OpenPopup("Create Profile"); - has_opened_ = true; - } - - auto profile_manager = - emulator_->kernel_state()->xam_state()->profile_manager(); - - bool dialog_open = true; - if (!ImGui::BeginPopupModal("Create Profile", &dialog_open, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { - Close(); - return; - } - - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - !ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0)) { - ImGui::SetKeyboardFocusHere(0); - } - - ImGui::TextUnformatted("Gamertag:"); - ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_)); - - const std::string gamertag_string = std::string(gamertag_); - bool valid = profile_manager->IsGamertagValid(gamertag_string); - - ImGui::BeginDisabled(!valid); - if (ImGui::Button("Create")) { - if (!profile_manager->CreateProfile(gamertag_string, false)) { - XELOGE("Failed to create profile: {}", gamertag_string); - } - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - ImGui::EndDisabled(); - ImGui::SameLine(); - - if (ImGui::Button("Cancel")) { - std::fill(std::begin(gamertag_), std::end(gamertag_), '\0'); - dialog_open = false; - } - - if (!dialog_open) { - ImGui::CloseCurrentPopup(); - Close(); - ImGui::EndPopup(); - return; - } - ImGui::EndPopup(); -} - X_RESULT xeXamShowSigninUI(uint32_t user_index, uint32_t users_needed, uint32_t flags) { // Mask values vary. Probably matching user types? Local/remote? @@ -2040,27 +927,30 @@ X_RESULT xeXamShowSigninUI(uint32_t user_index, uint32_t users_needed, }); } - auto close = [](SigninDialog* dialog) -> void {}; + auto close = [](ui::SigninUI* dialog) -> void {}; const Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); - return xeXamDispatchDialogAsync( - new SigninDialog(imgui_drawer, users_needed), close); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + return xeXamDispatchDialogAsync( + new ui::SigninUI( + imgui_drawer, kernel_state()->xam_state()->profile_manager(), + emulator->input_system()->GetLastUsedSlot(), users_needed), + close); } X_RESULT xeXamShowCreateProfileUIEx(uint32_t user_index, dword_t unkn, char* unkn2_ptr) { Emulator* emulator = kernel_state()->emulator(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); if (cvars::headless) { return X_ERROR_SUCCESS; } - auto close = [](CreateProfileDialog* dialog) -> void {}; + auto close = [](ui::CreateProfileUI* dialog) -> void {}; - return xeXamDispatchDialogAsync( - new CreateProfileDialog(imgui_drawer, emulator), close); + return xeXamDispatchDialogAsync( + new ui::CreateProfileUI(imgui_drawer, emulator), close); } dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t flags) { @@ -2104,16 +994,51 @@ dword_result_t XamShowAchievementsUI_entry(dword_t user_index, return X_ERROR_NO_SUCH_USER; } - ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer(); + xe::ui::ImGuiDrawer* imgui_drawer = + kernel_state()->emulator()->imgui_drawer(); - auto close = [](GameAchievementsDialog* dialog) -> void {}; - return xeXamDispatchDialogAsync( - new GameAchievementsDialog(imgui_drawer, ImVec2(100.f, 100.f), + auto close = [](ui::GameAchievementsUI* dialog) -> void {}; + return xeXamDispatchDialogAsync( + new ui::GameAchievementsUI(imgui_drawer, ImVec2(100.f, 100.f), &info.value(), user), close); } DECLARE_XAM_EXPORT1(XamShowAchievementsUI, kUserProfiles, kStub); +dword_result_t XamShowGamerCardUI_entry(dword_t user_index) { + auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return X_ERROR_ACCESS_DENIED; + } + + xe::ui::ImGuiDrawer* imgui_drawer = + kernel_state()->emulator()->imgui_drawer(); + + auto close = [](ui::GamercardUI* dialog) -> void {}; + return xeXamDispatchDialogAsync( + new ui::GamercardUI(kernel_state()->emulator()->display_window(), + imgui_drawer, kernel_state(), user->xuid()), + close); +} +DECLARE_XAM_EXPORT1(XamShowGamerCardUI, kUserProfiles, kImplemented); + +dword_result_t XamShowEditProfileUI_entry(dword_t user_index) { + auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return X_ERROR_ACCESS_DENIED; + } + + xe::ui::ImGuiDrawer* imgui_drawer = + kernel_state()->emulator()->imgui_drawer(); + + auto close = [](ui::GamercardUI* dialog) -> void {}; + return xeXamDispatchDialogAsync( + new ui::GamercardUI(kernel_state()->emulator()->display_window(), + imgui_drawer, kernel_state(), user->xuid()), + close); +} +DECLARE_XAM_EXPORT1(XamShowEditProfileUI, kUserProfiles, kImplemented); + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/xam_ui.h b/src/xenia/kernel/xam/xam_ui.h new file mode 100644 index 000000000..5989a2521 --- /dev/null +++ b/src/xenia/kernel/xam/xam_ui.h @@ -0,0 +1,126 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XAM_UI_H_ +#define XENIA_KERNEL_XAM_XAM_UI_H_ + +#include "xenia/kernel/util/shim_utils.h" +#include "xenia/ui/imgui_dialog.h" +#include "xenia/ui/imgui_drawer.h" + +namespace xe { +namespace kernel { +namespace xam { + +class XamDialog : public xe::ui::ImGuiDialog { + public: + void set_close_callback(std::function close_callback) { + close_callback_ = close_callback; + } + + protected: + XamDialog(xe::ui::ImGuiDrawer* imgui_drawer) + : xe::ui::ImGuiDialog(imgui_drawer) {} + + virtual ~XamDialog() {} + void OnClose() override { + if (close_callback_) { + close_callback_(); + } + } + + private: + std::function close_callback_ = nullptr; +}; + +class MessageBoxDialog : public XamDialog { + public: + MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, + std::string& description, std::vector buttons, + uint32_t default_button) + : XamDialog(imgui_drawer), + title_(title), + description_(description), + buttons_(std::move(buttons)), + default_button_(default_button), + chosen_button_(default_button) { + if (!title_.size()) { + title_ = "Message Box"; + } + } + + uint32_t chosen_button() const { return chosen_button_; } + + void OnDraw(ImGuiIO& io) override; + virtual ~MessageBoxDialog() {} + + private: + bool has_opened_ = false; + std::string title_; + std::string description_; + std::vector buttons_; + uint32_t default_button_ = 0; + uint32_t chosen_button_ = 0; +}; + +class KeyboardInputDialog : public XamDialog { + public: + KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, + std::string& description, std::string& default_text, + size_t max_length) + : XamDialog(imgui_drawer), + title_(title), + description_(description), + default_text_(default_text), + max_length_(max_length), + text_buffer_() { + if (!title_.size()) { + if (!description_.size()) { + title_ = "Keyboard Input"; + } else { + title_ = description_; + description_ = ""; + } + } + text_ = default_text; + text_buffer_.resize(max_length); + xe::string_util::copy_truncating(text_buffer_.data(), default_text_, + text_buffer_.size()); + } + virtual ~KeyboardInputDialog() {} + + const std::string& text() const { return text_; } + bool cancelled() const { return cancelled_; } + + void OnDraw(ImGuiIO& io) override; + + private: + bool has_opened_ = false; + std::string title_; + std::string description_; + std::string default_text_; + size_t max_length_ = 0; + std::vector text_buffer_; + std::string text_ = ""; + bool cancelled_ = true; +}; + +bool xeDrawProfileContent(xe::ui::ImGuiDrawer* imgui_drawer, + const uint64_t xuid, const uint8_t user_index, + const X_XAMACCOUNTINFO* account, + const xe::ui::ImmediateTexture* profile_icon, + std::function context_menu, + std::function on_profile_change, + uint64_t* selected_xuid); + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index a5802f7b8..54862a006 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -461,8 +461,21 @@ dword_result_t XamUserContentRestrictionCheckAccess_entry( } DECLARE_XAM_EXPORT1(XamUserContentRestrictionCheckAccess, kUserProfiles, kStub); -dword_result_t XamUserIsOnlineEnabled_entry(dword_t user_index) { return 1; } -DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub); +dword_result_t XamUserIsOnlineEnabled_entry(dword_t user_index) { + if (user_index >= XUserMaxUserCount) { + return 0; + } + + if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { + return 0; + } + + return kernel_state() + ->xam_state() + ->GetUserProfile(user_index) + ->IsLiveEnabled(); +} +DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kImplemented); dword_result_t XamUserGetMembershipTier_entry(dword_t user_index) { if (user_index >= XUserMaxUserCount) { @@ -473,9 +486,23 @@ dword_result_t XamUserGetMembershipTier_entry(dword_t user_index) { return X_ERROR_NO_SUCH_USER; } - return X_XAMACCOUNTINFO::AccountSubscriptionTier::kSubscriptionTierGold; + return kernel_state() + ->xam_state() + ->GetUserProfile(user_index) + ->GetSubscriptionTier(); } -DECLARE_XAM_EXPORT1(XamUserGetMembershipTier, kUserProfiles, kStub); +DECLARE_XAM_EXPORT1(XamUserGetMembershipTier, kUserProfiles, kImplemented); + +dword_result_t XamUserGetMembershipTierFromXUID_entry(qword_t xuid) { + const auto profile = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!profile) { + return 0; + } + + return profile->GetSubscriptionTier(); +} +DECLARE_XAM_EXPORT1(XamUserGetMembershipTierFromXUID, kUserProfiles, + kImplemented); dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1, dword_t unk2, lpdword_t out_value, @@ -868,15 +895,25 @@ dword_result_t XamUserIsUnsafeProgrammingAllowed_entry(dword_t user_index, DECLARE_XAM_EXPORT1(XamUserIsUnsafeProgrammingAllowed, kUserProfiles, kStub); dword_result_t XamUserGetSubscriptionType_entry(dword_t user_index, - dword_t unk2, dword_t unk3) { + lpdword_t subscription_ptr, + lpdword_t r5, + dword_t overlapped_ptr) { if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } - if (!unk2 || !unk3) { + if (!subscription_ptr || !r5) { return X_E_INVALIDARG; } + auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return X_ERROR_INVALID_PARAMETER; + } + + *subscription_ptr = user->GetSubscriptionTier(); + *r5 = 0x0; + return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetSubscriptionType, kUserProfiles, kStub); @@ -894,12 +931,11 @@ dword_result_t XamUserGetUserFlags_entry(dword_t user_index) { DECLARE_XAM_EXPORT1(XamUserGetUserFlags, kUserProfiles, kImplemented); dword_result_t XamUserGetUserFlagsFromXUID_entry(qword_t xuid) { - if (!kernel_state()->xam_state()->IsUserSignedIn(xuid)) { + const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user_profile) { return 0; } - const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(xuid); - return user_profile->GetCachedFlags(); } DECLARE_XAM_EXPORT1(XamUserGetUserFlagsFromXUID, kUserProfiles, kImplemented); @@ -961,6 +997,45 @@ dword_result_t XamUserCreateStatsEnumerator_entry( } DECLARE_XAM_EXPORT1(XamUserCreateStatsEnumerator, kUserProfiles, kSketchy); +dword_result_t XamUserGetUserTenure_entry(dword_t user_index, + lpdword_t tenure_level_ptr, + lpdword_t milestone_ptr, + lpqword_t milestone_date_ptr, + dword_t overlap_ptr) { + if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { + return X_E_INVALIDARG; + } + + const auto& user_profile = + kernel_state()->xam_state()->GetUserProfile(user_index); + + if (const auto setting = + kernel_state()->xam_state()->user_tracker()->GetSetting( + user_profile, kDashboardID, + static_cast(UserSettingId::XPROFILE_TENURE_LEVEL))) { + *tenure_level_ptr = std::get(setting->get_host_data()); + } + + if (const auto setting = + kernel_state()->xam_state()->user_tracker()->GetSetting( + user_profile, kDashboardID, + static_cast( + UserSettingId::XPROFILE_TENURE_MILESTONE))) { + *milestone_ptr = std::get(setting->get_host_data()); + } + + if (const auto setting = + kernel_state()->xam_state()->user_tracker()->GetSetting( + user_profile, kDashboardID, + static_cast( + UserSettingId::XPROFILE_TENURE_NEXT_MILESTONE_DATE))) { + *milestone_date_ptr = std::get(setting->get_host_data()); + } + + return X_ERROR_SUCCESS; +} +DECLARE_XAM_EXPORT1(XamUserGetUserTenure, kUserProfiles, kImplemented); + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/xdbf/spa_info.h b/src/xenia/kernel/xam/xdbf/spa_info.h index a4de36cce..45a0a6fe8 100644 --- a/src/xenia/kernel/xam/xdbf/spa_info.h +++ b/src/xenia/kernel/xam/xdbf/spa_info.h @@ -38,6 +38,8 @@ enum class TitleType : uint32_t { kFull = 1, kDemo = 2, kDownload = 3, + kUnknown = 4, + kApp = 5 }; enum class TitleFlags { diff --git a/src/xenia/ui/imgui_drawer.h b/src/xenia/ui/imgui_drawer.h index 099dfb454..541d77252 100644 --- a/src/xenia/ui/imgui_drawer.h +++ b/src/xenia/ui/imgui_drawer.h @@ -37,7 +37,7 @@ class Window; using IconsData = std::map>; -constexpr ImVec2 default_image_icon_size = ImVec2(75.f, 75.f); +constexpr ImVec2 default_image_icon_size = ImVec2(64.f, 64.f); class ImGuiDrawer : public WindowInputListener, public UIDrawer { public: diff --git a/src/xenia/xbox.h b/src/xenia/xbox.h index 448939854..e89e3a4a4 100644 --- a/src/xenia/xbox.h +++ b/src/xenia/xbox.h @@ -547,6 +547,117 @@ enum class XLanguage : uint32_t { kMaxLanguages = 13 }; +enum class XOnlineCountry : uint32_t { + kUnitedArabEmirates = 1, + kAlbania = 2, + kArmenia = 3, + kArgentina = 4, + kAustria = 5, + kAustralia = 6, + kAzerbaijan = 7, + kBelgium = 8, + kBulgaria = 9, + kBahrain = 10, + kBruneiDarussalam = 11, + kBolivia = 12, + kBrazil = 13, + kBelarus = 14, + kBelize = 15, + kCanada = 16, + kSwitzerland = 18, + kChile = 19, + kChina = 20, + kColombia = 21, + kCostaRica = 22, + kCzechRepublic = 23, + kGermany = 24, + kDenmark = 25, + kDominicanRepublic = 26, + kAlgeria = 27, + kEcuador = 28, + kEstonia = 29, + kEgypt = 30, + kSpain = 31, + kFinland = 32, + kFaroeIslands = 33, + kFrance = 34, + kGreatBritain = 35, + kGeorgia = 36, + kGreece = 37, + kGuatemala = 38, + kHongKong = 39, + kHonduras = 40, + kCroatia = 41, + kHungary = 42, + kIndonesia = 43, + kIreland = 44, + kIsrael = 45, + kIndia = 46, + kIraq = 47, + kIran = 48, + kIceland = 49, + kItaly = 50, + kJamaica = 51, + kJordan = 52, + kJapan = 53, + kKenya = 54, + kKyrgyzstan = 55, + kKorea = 56, + kKuwait = 57, + kKazakhstan = 58, + kLebanon = 59, + kLiechtenstein = 60, + kLithuania = 61, + kLuxembourg = 62, + kLatvia = 63, + kLibya = 64, + kMorocco = 65, + kMonaco = 66, + kMacedonia = 67, + kMongolia = 68, + kMacau = 69, + kMaldives = 70, + kMexico = 71, + kMalaysia = 72, + kNicaragua = 73, + kNetherlands = 74, + kNorway = 75, + kNewZealand = 76, + kOman = 77, + kPanama = 78, + kPeru = 79, + kPhilippines = 80, + kPakistan = 81, + kPoland = 82, + kPuertoRico = 83, + kPortugal = 84, + kParaguay = 85, + kQatar = 86, + kRomania = 87, + kRussianFederation = 88, + kSaudiArabia = 89, + kSweden = 90, + kSingapore = 91, + kSlovenia = 92, + kSlovakRepublic = 93, + kElSalvador = 95, + kSyria = 96, + kThailand = 97, + kTunisia = 98, + kTurkey = 99, + kTrinidadAndTobago = 100, + kTaiwan = 101, + kUkraine = 102, + kUnitedStates = 103, + kUruguay = 104, + kUzbekistan = 105, + kVenezuela = 106, + kVietNam = 107, + kYemen = 108, + kSouthAfrica = 109, + kZimbabwe = 110 +}; + enum class XContentType : uint32_t { kInvalid = 0x00000000, kSavedGame = 0x00000001, @@ -706,20 +817,26 @@ struct X_XAMACCOUNTINFO { char passport_password[0x20]; char owner_passport_membername[0x72]; - bool IsPasscodeEnabled() { + bool IsPasscodeEnabled() const { return static_cast(reserved_flags & AccountReservedFlags::kPasswordProtected); } - bool IsLiveEnabled() { + bool IsLiveEnabled() const { return static_cast(reserved_flags & AccountReservedFlags::kLiveEnabled); } + uint64_t GetOnlineXUID() const { return xuid_online; } + + std::string_view GetOnlineDomain() const { + return std::string_view(online_domain); + } + uint32_t GetCachedFlags() const { return cached_user_flags; }; - uint32_t GetCountry() const { - return (cached_user_flags & kCountryMask) >> 8; + XOnlineCountry GetCountry() const { + return static_cast((cached_user_flags & kCountryMask) >> 8); } AccountSubscriptionTier GetSubscriptionTier() const { @@ -734,6 +851,35 @@ struct X_XAMACCOUNTINFO { std::string GetGamertagString() const { return xe::to_utf8(std::u16string(gamertag)); } + + void ToggleLiveFlag(bool is_live) { + reserved_flags = reserved_flags & ~AccountReservedFlags::kLiveEnabled; + + if (is_live) { + reserved_flags = reserved_flags | AccountReservedFlags::kLiveEnabled; + } + } + + void SetCountry(XOnlineCountry country) { + cached_user_flags = cached_user_flags & ~kCountryMask; + cached_user_flags = cached_user_flags | + (static_cast(country) << 8) & kCountryMask; + } + + void SetLanguage(XLanguage language) { + cached_user_flags = cached_user_flags & ~kLanguageMask; + + cached_user_flags = cached_user_flags | + (static_cast(language) << 25) & kLanguageMask; + } + + void SetSubscriptionTier(AccountSubscriptionTier sub_tier) { + cached_user_flags = cached_user_flags & ~kSubscriptionTierMask; + + cached_user_flags = + cached_user_flags | + (static_cast(sub_tier) << 20) & kSubscriptionTierMask; + } }; static_assert_size(X_XAMACCOUNTINFO, 0x17C); #pragma pack(pop)