[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

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