[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
This commit is contained in:
Gliniak 2025-05-10 12:52:05 +02:00 committed by Radosław Gliński
parent 270e88a7a7
commit e601b5ab87
30 changed files with 2904 additions and 1404 deletions

View File

@ -7,82 +7,24 @@
******************************************************************************
*/
#include "xenia/app/profile_dialogs.h"
#include <algorithm>
#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<int>(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();

View File

@ -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<uint64_t, std::unique_ptr<ui::ImmediateTexture>> profile_icon_;
uint64_t selected_xuid_ = 0;
EmulatorWindow* emulator_window_;
};

View File

@ -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<uint16_t, uint16_t> 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<uint8_t> 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<uint8_t> data(file_size);
fread(data.data(), 1, file_size, file);
fclose(file);
return data;
}
} // namespace xe

View File

@ -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 <filesystem>
#include <utility>
#include <vector>
namespace xe {
bool IsFilePngImage(const std::filesystem::path& file_path);
std::pair<uint16_t, uint16_t> GetImageResolution(
const std::filesystem::path& file_path);
std::vector<uint8_t> ReadPngFromFile(const std::filesystem::path& file_path);
} // namespace xe
#endif // XENIA_BASE_PNG_UTILS_H_

View File

@ -10,9 +10,7 @@
#include "xenia/kernel/xam/profile_manager.h"
#include <filesystem>
#include <vector>
#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<uint64_t> 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<uint8_t>(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;
}

View File

@ -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<uint64_t> 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;

View File

@ -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

View File

@ -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

View File

@ -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<uint32_t>(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<uint32_t>(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<ImTextureID>(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

View File

@ -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<Achievement> achievements_info_;
std::map<uint32_t, std::unique_ptr<xe::ui::ImmediateTexture>>
achievements_icons_;
};
} // namespace ui
} // namespace xam
} // namespace kernel
} // namespace xe
#endif

View File

@ -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<UserSettingId, 19> 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<std::u16string>(
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<uint32_t>(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<bool(std::span<char>)> 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<int32_t>(gamercardValues_.gpd_settings[setting_id]);
ImGui::Combo(fmt::format("###{}", label).c_str(),
reinterpret_cast<int*>(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<const char> 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<ImTextureID>(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<int*>(&gamercardValues_.language),
XLanguageName, std::size(XLanguageName));
ImGui::Text("Country:");
ImGui::SameLine(leftSideTextObjectAlignment);
ImGui::Combo("###Country", reinterpret_cast<int*>(&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<int*>(&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

View File

@ -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<UserSettingId, UserDataTypes> gpd_settings;
// GPD string buffers
char gamer_name[0x104];
char gamer_motto[0x2C];
char gamer_bio[kMaxUserDataSize];
// Other
std::vector<uint8_t> 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<bool(std::span<char>)> 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

View File

@ -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

View File

@ -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<std::string, uint16_t> 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

View File

@ -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<const char*> combo_items;
int items_count = 0;
int current_item = 0;
// Fill slot list.
std::vector<uint8_t> 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<int>(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<int>(combo_items.size());
ImGui::BeginDisabled(users_needed_ == 1);
ImGui::Combo(fmt::format("##Slot{:d}", i).c_str(), &current_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<uint64_t> 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<int>(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<int>(combo_items.size());
ImGui::BeginDisabled(chosen_slots_[i] == 0xFF);
ImGui::Combo(fmt::format("##Profile{:d}", i).c_str(), &current_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<uint8_t, uint64_t> 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

View File

@ -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<uint8_t, std::string> 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<std::pair<uint64_t, std::string>> 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

View File

@ -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<ImTextureID>(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

View File

@ -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<uint32_t, std::unique_ptr<xe::ui::ImmediateTexture>> title_icon;
std::vector<TitleInfo> info_;
};
} // namespace ui
} // namespace xam
} // namespace kernel
} // namespace xe
#endif

View File

@ -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<const char16_t*>(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;

View File

@ -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<const uint8_t> 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<uint8_t>(icon_data.begin(), icon_data.end()));
}
std::vector<uint8_t> UserProfile::LoadGpd(const uint32_t title_id) {

View File

@ -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<XTileType, std::string> kTileFileNames = {
{XTileType::kAvatarGamerTileSmall, "avtr_32.png"},
};
static constexpr std::pair<uint16_t, uint16_t> kProfileIconSize = {64, 64};
static constexpr std::pair<uint16_t, uint16_t> 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<const uint8_t> 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<const uint8_t> icon_data);
std::vector<uint8_t> LoadGpd(const uint32_t title_id);
bool WriteGpd(const uint32_t title_id);
};

View File

@ -379,7 +379,7 @@ const static std::set<UserSettingId> 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:

View File

@ -14,6 +14,7 @@
#include <sstream>
#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<const uint8_t> 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<int>(icon_data.size()), &width,
&height, &channels)) {
return false;
}
XTileType icon_type = XTileType::kGameIcon;
if (std::pair<uint16_t, uint16_t>(width, height) == kProfileIconSize) {
icon_type = XTileType::kGamerTile;
} else if (std::pair<uint16_t, uint16_t>(width, height) ==
kProfileIconSizeSmall) {
icon_type = XTileType::kGamerTileSmall;
} else {
return false;
}
user->WriteProfileIcon(icon_type, icon_data);
return true;
}
std::span<const uint8_t> UserTracker::GetIcon(uint64_t xuid, uint32_t title_id,
XTileType tile_type,
uint64_t tile_id) const {

View File

@ -75,6 +75,9 @@ class UserTracker {
void UpsertSetting(uint64_t xuid, uint32_t title_id,
const UserSetting* setting);
std::optional<UserSetting> 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<const uint8_t> icon_data);
std::span<const uint8_t> 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<UserSetting> GetSetting(UserProfile* user, uint32_t title_id,
uint32_t setting_id) const;
std::optional<UserSetting> GetGpdSetting(UserProfile* user, uint32_t title_id,
uint32_t setting_id) const;

File diff suppressed because it is too large Load Diff

View File

@ -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<void()> 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<void()> close_callback_ = nullptr;
};
class MessageBoxDialog : public XamDialog {
public:
MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title,
std::string& description, std::vector<std::string> 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<std::string> 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<char> 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<bool()> context_menu,
std::function<void()> on_profile_change,
uint64_t* selected_xuid);
} // namespace xam
} // namespace kernel
} // namespace xe
#endif

View File

@ -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<uint32_t>(UserSettingId::XPROFILE_TENURE_LEVEL))) {
*tenure_level_ptr = std::get<int32_t>(setting->get_host_data());
}
if (const auto setting =
kernel_state()->xam_state()->user_tracker()->GetSetting(
user_profile, kDashboardID,
static_cast<uint32_t>(
UserSettingId::XPROFILE_TENURE_MILESTONE))) {
*milestone_ptr = std::get<int32_t>(setting->get_host_data());
}
if (const auto setting =
kernel_state()->xam_state()->user_tracker()->GetSetting(
user_profile, kDashboardID,
static_cast<uint32_t>(
UserSettingId::XPROFILE_TENURE_NEXT_MILESTONE_DATE))) {
*milestone_date_ptr = std::get<int64_t>(setting->get_host_data());
}
return X_ERROR_SUCCESS;
}
DECLARE_XAM_EXPORT1(XamUserGetUserTenure, kUserProfiles, kImplemented);
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -38,6 +38,8 @@ enum class TitleType : uint32_t {
kFull = 1,
kDemo = 2,
kDownload = 3,
kUnknown = 4,
kApp = 5
};
enum class TitleFlags {

View File

@ -37,7 +37,7 @@ class Window;
using IconsData = std::map<uint32_t, std::span<const uint8_t>>;
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:

View File

@ -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<bool>(reserved_flags &
AccountReservedFlags::kPasswordProtected);
}
bool IsLiveEnabled() {
bool IsLiveEnabled() const {
return static_cast<bool>(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<XOnlineCountry>((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<uint32_t>(country) << 8) & kCountryMask;
}
void SetLanguage(XLanguage language) {
cached_user_flags = cached_user_flags & ~kLanguageMask;
cached_user_flags = cached_user_flags |
(static_cast<uint32_t>(language) << 25) & kLanguageMask;
}
void SetSubscriptionTier(AccountSubscriptionTier sub_tier) {
cached_user_flags = cached_user_flags & ~kSubscriptionTierMask;
cached_user_flags =
cached_user_flags |
(static_cast<uint32_t>(sub_tier) << 20) & kSubscriptionTierMask;
}
};
static_assert_size(X_XAMACCOUNTINFO, 0x17C);
#pragma pack(pop)