From 9bdd07590c7a325917e49332ceca90cc085c547d Mon Sep 17 00:00:00 2001 From: Gliniak Date: Sat, 28 Sep 2024 10:42:32 +0200 Subject: [PATCH] [XAM] Implementation of: Profile Manager & Profiles Support Thanks Emoose for Account Encrypt/Decrypt code! --- src/xenia/app/emulator_window.cc | 89 ++- src/xenia/app/emulator_window.h | 13 +- src/xenia/app/profile_dialogs.cc | 333 +++++++++++ src/xenia/app/profile_dialogs.h | 76 +++ src/xenia/emulator.cc | 168 +++++- src/xenia/emulator.h | 3 + src/xenia/hid/input_system.cc | 12 +- src/xenia/hid/input_system.h | 8 +- src/xenia/hid/sdl/sdl_input_driver.cc | 2 +- src/xenia/hid/xinput/xinput_input_driver.cc | 2 +- src/xenia/kernel/kernel_state.cc | 8 +- src/xenia/kernel/xam/content_manager.cc | 127 +++-- src/xenia/kernel/xam/content_manager.h | 47 +- src/xenia/kernel/xam/profile_manager.cc | 534 ++++++++++++++++++ src/xenia/kernel/xam/profile_manager.h | 142 +++++ src/xenia/kernel/xam/user_profile.cc | 12 +- src/xenia/kernel/xam/user_profile.h | 7 +- src/xenia/kernel/xam/xam_content.cc | 139 ++++- src/xenia/kernel/xam/xam_content_aggregate.cc | 6 +- src/xenia/kernel/xam/xam_content_device.cc | 2 +- src/xenia/kernel/xam/xam_input.cc | 27 +- src/xenia/kernel/xam/xam_state.cc | 46 +- src/xenia/kernel/xam/xam_state.h | 8 +- src/xenia/kernel/xam/xam_ui.cc | 41 +- src/xenia/kernel/xam/xam_user.cc | 131 ++--- src/xenia/vfs/devices/stfs_xbox.h | 3 +- .../vfs/devices/xcontent_container_device.h | 2 + src/xenia/xbox.h | 85 +++ 28 files changed, 1775 insertions(+), 298 deletions(-) create mode 100644 src/xenia/app/profile_dialogs.cc create mode 100644 src/xenia/app/profile_dialogs.h create mode 100644 src/xenia/kernel/xam/profile_manager.cc create mode 100644 src/xenia/kernel/xam/profile_manager.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index f1b874c9a..c2bf77867 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -36,7 +36,9 @@ #include "xenia/gpu/d3d12/d3d12_command_processor.h" #include "xenia/gpu/graphics_system.h" #include "xenia/hid/input_system.h" +#include "xenia/kernel/xam/profile_manager.h" #include "xenia/kernel/xam/xam_module.h" +#include "xenia/kernel/xam/xam_state.h" #include "xenia/ui/file_picker.h" #include "xenia/ui/graphics_provider.h" #include "xenia/ui/imgui_dialog.h" @@ -147,6 +149,14 @@ DEFINE_int32(recent_titles_entry_amount, 10, "recently played titles.", "General"); +namespace xe { +namespace kernel { +namespace xam { +extern std::atomic xam_dialogs_shown_; +} +} // namespace kernel +} // namespace xe + namespace xe { namespace app { @@ -250,6 +260,14 @@ void EmulatorWindow::ShutdownGraphicsSystemPresenterPainting() { } void EmulatorWindow::OnEmulatorInitialized() { + if (!emulator_->kernel_state() + ->xam_state() + ->profile_manager() + ->GetProfilesCount()) { + new NoProfileDialog(imgui_drawer_.get(), this); + disable_hotkeys_ = true; + } + emulator_initialized_ = true; window_->SetMainMenuEnabled(true); // When the user can see that the emulator isn't initializing anymore (the @@ -593,6 +611,15 @@ bool EmulatorWindow::Initialize() { } main_menu->AddChild(std::move(file_menu)); + // Profile Menu + auto profile_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Profile"); + { + profile_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Show Profile Menu", "", + std::bind(&EmulatorWindow::ToggleProfilesConfigDialog, this))); + } + main_menu->AddChild(std::move(profile_menu)); + // CPU menu. auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU"); { @@ -1104,6 +1131,10 @@ void EmulatorWindow::InstallContent() { summary += "\n"; } + if (content_installation_details.count(XContentType::kProfile)) { + emulator_->kernel_state()->xam_state()->profile_manager()->ReloadProfiles(); + } + xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Content Installation Summary", summary); } @@ -1296,24 +1327,13 @@ void EmulatorWindow::CreateZarchive() { } void EmulatorWindow::ShowContentDirectory() { - std::filesystem::path target_path; - auto content_root = emulator_->content_root(); - if (!emulator_->is_title_open() || !emulator_->kernel_state()) { - target_path = content_root; - } else { - // TODO(gibbed): expose this via ContentManager? - auto title_id = - fmt::format("{:08X}", emulator_->kernel_state()->title_id()); - auto package_root = content_root / title_id; - target_path = package_root; + + if (!std::filesystem::exists(content_root)) { + std::filesystem::create_directories(content_root); } - if (!std::filesystem::exists(target_path)) { - std::filesystem::create_directories(target_path); - } - - LaunchFileExplorer(target_path); + LaunchFileExplorer(content_root); } void EmulatorWindow::CpuTimeScalarReset() { @@ -1382,6 +1402,23 @@ void EmulatorWindow::ToggleDisplayConfigDialog() { } } +void EmulatorWindow::ToggleProfilesConfigDialog() { + if (!profile_config_dialog_) { + disable_hotkeys_ = true; + emulator_->kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, + 1); + profile_config_dialog_ = + std::make_unique(imgui_drawer_.get(), this); + kernel::xam::xam_dialogs_shown_++; + } else { + disable_hotkeys_ = false; + emulator_->kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, + 0); + profile_config_dialog_.reset(); + kernel::xam::xam_dialogs_shown_--; + } +} + void EmulatorWindow::ToggleControllerVibration() { auto input_sys = emulator()->input_system(); if (input_sys) { @@ -1579,7 +1616,13 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey( // Default return value EmulatorWindow::ControllerHotKey Unknown_hotkey = {}; - if (buttons == 0) return Unknown_hotkey; + if (buttons == 0) { + return Unknown_hotkey; + } + + if (disable_hotkeys_.load()) { + return Unknown_hotkey; + } // Hotkey cool-down to prevent toggling too fast const std::chrono::milliseconds delay(75); @@ -1790,7 +1833,8 @@ void EmulatorWindow::GamepadHotKeys() { while (true) { auto input_lock = input_sys->lock(); - for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) { + for (uint32_t user_index = 0; user_index < XUserMaxUserCount; + ++user_index) { X_RESULT result = input_sys->GetState(user_index, &state); // Release the lock before processing the hotkey @@ -1962,6 +2006,17 @@ xe::X_STATUS EmulatorWindow::RunTitle( auto result = emulator_->LaunchPath(abs_path); + disable_hotkeys_ = false; + + if (profile_config_dialog_) { + profile_config_dialog_.reset(); + kernel::xam::xam_dialogs_shown_--; + } + + if (display_config_dialog_) { + display_config_dialog_.reset(); + } + imgui_drawer_.get()->ClearDialogs(); if (result) { diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 86bae14ca..a39f7c91f 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -25,7 +25,7 @@ #include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" -#define MAX_USERS 4 +#include "xenia/app/profile_dialogs.h" namespace xe { namespace app { @@ -93,6 +93,12 @@ class EmulatorWindow { void SaveImage(const std::filesystem::path& path, const xe::ui::RawImage& image); + void ToggleProfilesConfigDialog(); + void SetHotkeysState(bool enabled) { disable_hotkeys_ = !enabled; } + // We need to store it somewhere so there will be no situation when there are + // multiple instances opened. + std::unique_ptr profile_creation_dialog_; + // Types of button functions for hotkeys. enum class ButtonFunctions { ToggleFullscreen, @@ -257,12 +263,17 @@ class EmulatorWindow { std::unique_ptr immediate_drawer_; bool emulator_initialized_ = false; + std::atomic disable_hotkeys_ = false; std::string base_title_; bool initializing_shader_storage_ = false; std::unique_ptr display_config_dialog_; + // Storing pointers and toggling dialog state is useful for broadcasting + // messages back to guest. + std::unique_ptr profile_config_dialog_; + std::vector recently_launched_titles_; }; diff --git a/src/xenia/app/profile_dialogs.cc b/src/xenia/app/profile_dialogs.cc new file mode 100644 index 000000000..2b0bbf717 --- /dev/null +++ b/src/xenia/app/profile_dialogs.cc @@ -0,0 +1,333 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/app/profile_dialogs.h" +#include +#include "xenia/app/emulator_window.h" +#include "xenia/base/system.h" + +namespace xe { +namespace app { + +void CreateProfileDialog::OnDraw(ImGuiIO& io) { + auto profile_manager = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager(); + + const auto window_position = + ImVec2(GetIO().DisplaySize.x * 0.40f, GetIO().DisplaySize.y * 0.44f); + + ImGui::SetNextWindowPos(window_position, ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(1.0f); + + bool dialog_open = true; + if (!ImGui::Begin("Create Profile", &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::End(); + emulator_window_->profile_creation_dialog_.reset(); + 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); + + if (profile_manager->IsGamertagValid(gamertag_string)) { + if (ImGui::Button("Create")) { + if (profile_manager->CreateProfile(gamertag_string, migration) && + migration) { + emulator_window_->emulator()->DataMigration(0xB13EBABEBABEBABE); + } + std::fill(std::begin(gamertag), std::end(gamertag), '\0'); + dialog_open = false; + } + ImGui::SameLine(); + } + + if (ImGui::Button("Cancel")) { + std::fill(std::begin(gamertag), std::end(gamertag), '\0'); + dialog_open = false; + } + + if (!dialog_open) { + ImGui::End(); + emulator_window_->profile_creation_dialog_.reset(); + return; + } + ImGui::End(); +} + +void NoProfileDialog::OnDraw(ImGuiIO& io) { + auto profile_manager = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager(); + + if (profile_manager->GetProfilesCount()) { + delete this; + return; + } + + const auto window_position = + ImVec2(GetIO().DisplaySize.x * 0.35f, GetIO().DisplaySize.y * 0.4f); + + ImGui::SetNextWindowPos(window_position, ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(1.0f); + + bool dialog_open = true; + if (!ImGui::Begin("No Profiles Found", &dialog_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::End(); + delete this; + return; + } + + const std::string message = + "There is no profile available! You will not be able to save without " + "one.\n\nWould you like to create one?"; + + ImGui::TextUnformatted(message.c_str()); + + ImGui::Separator(); + ImGui::NewLine(); + + const auto content_files = xe::filesystem::ListDirectories( + emulator_window_->emulator()->content_root()); + + if (content_files.empty()) { + if (ImGui::Button("Create Profile") && + !emulator_window_->profile_creation_dialog_) { + emulator_window_->profile_creation_dialog_ = + std::make_unique( + emulator_window_->imgui_drawer(), emulator_window_); + } + } else { + if (ImGui::Button("Create profile & migrate data") && + !emulator_window_->profile_creation_dialog_) { + emulator_window_->profile_creation_dialog_ = + std::make_unique( + emulator_window_->imgui_drawer(), emulator_window_, true); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Open profile menu")) { + emulator_window_->ToggleProfilesConfigDialog(); + } + + ImGui::SameLine(); + if (ImGui::Button("Close") || !dialog_open) { + emulator_window_->SetHotkeysState(true); + ImGui::End(); + delete this; + return; + } + ImGui::End(); +} + +void ProfileConfigDialog::OnDraw(ImGuiIO& io) { + if (!emulator_window_->emulator() || + !emulator_window_->emulator()->kernel_state() || + !emulator_window_->emulator()->kernel_state()->xam_state()) { + return; + } + + auto profile_manager = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager(); + if (!profile_manager) { + return; + } + + auto profiles = profile_manager->GetProfiles(); + + ImGui::SetNextWindowPos(ImVec2(40, 40), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(0.8f); + + bool dialog_open = true; + if (!ImGui::Begin("Profiles Menu", &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::End(); + return; + } + + if (profiles->empty()) { + ImGui::TextUnformatted("No profiles found!"); + ImGui::Spacing(); + ImGui::Separator(); + } + + for (auto& [xuid, account] : *profiles) { + ImGui::PushID(static_cast(xuid)); + + const uint8_t user_index = + profile_manager->GetUserIndexAssignedToProfile(xuid); + + DrawProfileContent(xuid, user_index, &account); + + ImGui::PopID(); + ImGui::Spacing(); + ImGui::Separator(); + } + + ImGui::Spacing(); + + if (ImGui::Button("Create Profile") && + !emulator_window_->profile_creation_dialog_) { + emulator_window_->profile_creation_dialog_ = + std::make_unique(emulator_window_->imgui_drawer(), + emulator_window_); + } + + ImGui::End(); + + if (!dialog_open) { + emulator_window_->ToggleProfilesConfigDialog(); + return; + } +} + +bool ProfileConfigDialog::DrawProfileContent(const uint64_t xuid, + const uint8_t user_index, + const X_XAMACCOUNTINFO* account) { + auto profile_manager = emulator_window_->emulator() + ->kernel_state() + ->xam_state() + ->profile_manager(); + + const float default_image_size = 75.0f; + auto position = ImGui::GetCursorPos(); + const float selectable_height = + ImGui::GetTextLineHeight() * + 5; // 3 is for amount of lines of text behind image/object. + const auto font = emulator_window_->imgui_drawer()->GetIO().Fonts->Fonts[0]; + + const auto text_size = font->CalcTextSizeA( + font->FontSize, FLT_MAX, -1.0f, + fmt::format("XUID: {:016X}\n", 0xB13EBABEBABEBABE).c_str()); + + const auto image_scale = selectable_height / default_image_size; + const auto image_size = ImVec2(default_image_size * image_scale, + default_image_size * image_scale); + // This includes 10% to include empty spaces between border and elements. + auto selectable_region_size = + ImVec2((image_size.x + text_size.x) * 1.10f, selectable_height); + + if (ImGui::Selectable("##Selectable", selected_xuid_ == xuid, + ImGuiSelectableFlags_SpanAllColumns, + selectable_region_size)) { + selected_xuid_ = xuid; + } + + if (ImGui::BeginPopupContextItem("Profile Menu")) { + if (user_index == static_cast(-1)) { + if (ImGui::MenuItem("Login")) { + profile_manager->Login(xuid); + } + + if (ImGui::BeginMenu("Login to slot:")) { + for (uint8_t i = 0; i < XUserMaxUserCount; i++) { + if (ImGui::MenuItem(fmt::format("slot {}", i).c_str())) { + profile_manager->Login(xuid, i); + } + } + ImGui::EndMenu(); + } + } else { + if (ImGui::MenuItem("Logout")) { + profile_manager->Logout(user_index); + } + } + + ImGui::MenuItem("Modify (unsupported)"); + ImGui::MenuItem("Show Achievements (unsupported)"); + + 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(); + } + + ImGui::SameLine(); + ImGui::SetCursorPos(position); + + // In the future it can be replaced with profile icon. + ImGui::Image(user_index < XUserMaxUserCount + ? imgui_drawer()->GetNotificationIcon(user_index) + : nullptr, + ImVec2(default_image_size * image_scale, + default_image_size * image_scale)); + + ImGui::SameLine(); + position = ImGui::GetCursorPos(); + ImGui::TextUnformatted( + fmt::format("User: {}\n", account->GetGamertagString()).c_str()); + + ImGui::SameLine(); + ImGui::SetCursorPos(position); + ImGui::SetCursorPosY(position.y + ImGui::GetTextLineHeight()); + ImGui::TextUnformatted(fmt::format("XUID: {:016X}\n", xuid).c_str()); + + if (user_index != static_cast(-1)) { + ImGui::SameLine(); + ImGui::SetCursorPos(position); + ImGui::SetCursorPosY(position.y + 2 * ImGui::GetTextLineHeight()); + ImGui::TextUnformatted( + fmt::format("Assigned to slot: {}\n", user_index).c_str()); + } + return true; +} + +} // namespace app +} // namespace xe \ No newline at end of file diff --git a/src/xenia/app/profile_dialogs.h b/src/xenia/app/profile_dialogs.h new file mode 100644 index 000000000..a8a9708b3 --- /dev/null +++ b/src/xenia/app/profile_dialogs.h @@ -0,0 +1,76 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_APP_PROFILE_DIALOGS_H_ +#define XENIA_APP_PROFILE_DIALOGS_H_ + +#include "xenia/ui/imgui_dialog.h" +#include "xenia/ui/imgui_drawer.h" +#include "xenia/xbox.h" + +namespace xe { +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 migration = false; + char gamertag[16] = ""; + EmulatorWindow* emulator_window_; +}; + +class NoProfileDialog final : public ui::ImGuiDialog { + public: + NoProfileDialog(ui::ImGuiDrawer* imgui_drawer, + EmulatorWindow* emulator_window) + : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) {} + + protected: + void OnDraw(ImGuiIO& io) override; + + EmulatorWindow* emulator_window_; +}; + +class ProfileConfigDialog final : public ui::ImGuiDialog { + public: + ProfileConfigDialog(ui::ImGuiDrawer* imgui_drawer, + EmulatorWindow* emulator_window) + : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) { + memset(gamertag, 0, sizeof(gamertag)); + } + + protected: + void OnDraw(ImGuiIO& io) override; + + private: + bool DrawProfileContent(const uint64_t xuid, const uint8_t user_index, + const X_XAMACCOUNTINFO* account); + + uint64_t selected_xuid_ = 0; + char gamertag[16] = ""; + EmulatorWindow* emulator_window_; +}; + +} // namespace app +} // namespace xe + +#endif diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index f9d9636ac..376088f8f 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -50,6 +50,7 @@ #include "xenia/ui/file_picker.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" +#include "xenia/ui/imgui_host_notification.h" #include "xenia/ui/window.h" #include "xenia/ui/windowed_app_context.h" #include "xenia/vfs/device.h" @@ -594,6 +595,163 @@ X_STATUS Emulator::LaunchDefaultModule(const std::filesystem::path& path) { return result; } +X_STATUS Emulator::DataMigration(const uint64_t xuid) { + uint32_t failure_count = 0; + const std::string xuid_string = fmt::format("{:016X}", xuid); + const std::string common_xuid_string = fmt::format("{:016X}", 0); + const std::filesystem::path path_to_profile_data = + content_root_ / xuid_string / "FFFE07D1" / "00010000" / xuid_string; + // Filter directories inside. First we need to find any content type + // directories. + // Savefiles must go to user specific directory + // Everything else goes to common + const auto titles_to_move = xe::filesystem::FilterByName( + xe::filesystem::ListDirectories(content_root_), + std::regex("[A-F0-9]{8}")); + + for (const auto& title : titles_to_move) { + if (xe::path_to_utf8(title.name) == "FFFE07D1" || + xe::path_to_utf8(title.name) == "00000000") { + // SKip any dashboard/profile related data that was previously installed + continue; + } + + const auto content_type_dirs = xe::filesystem::FilterByName( + xe::filesystem::ListDirectories(title.path / title.name), + std::regex("[A-F0-9]{8}")); + + for (const auto& content_type : content_type_dirs) { + const std::string used_xuid = + xe::path_to_utf8(content_type.name) == "00000001" + ? xuid_string + : common_xuid_string; + + const auto previous_path = content_root_ / title.name / content_type.name; + const auto path = content_root_ / used_xuid / title.name; + + if (!std::filesystem::exists(path)) { + std::filesystem::create_directories(path); + } + + std::error_code ec; + std::filesystem::rename(previous_path, path / content_type.name, ec); + + if (ec) { + failure_count++; + XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})", + __func__, xe::path_to_utf8(previous_path), + xe::path_to_utf8(path / content_type.name), ec.message(), + ec.value()); + } + } + // Other directories: + // Headers - Just copy everything to both common and xuid locations + // profile - ? + if (std::filesystem::exists(title.path / title.name / "Headers")) { + const auto xuid_path = + content_root_ / xuid_string / title.name / "Headers"; + + std::filesystem::create_directories(xuid_path); + + std::error_code ec; + // Copy to specific user + std::filesystem::copy(title.path / title.name / "Headers", xuid_path, + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::skip_existing, + ec); + if (ec) { + failure_count++; + XELOGW("{}: Copying from: {} to: {} failed! Error message: {} ({:08X})", + __func__, xe::path_to_utf8(title.path / title.name / "Headers"), + xe::path_to_utf8(xuid_path), ec.message(), ec.value()); + } + + const auto header_types = + xe::filesystem::ListDirectories(title.path / title.name / "Headers"); + + if (!(header_types.size() == 1 && + header_types.at(0).name == "00000001")) { + const auto common_path = + content_root_ / common_xuid_string / title.name / "Headers"; + + std::filesystem::create_directories(common_path); + + // Copy to common, skip cases where only savefile header is available + std::filesystem::copy(title.path / title.name / "Headers", common_path, + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::skip_existing, + ec); + if (ec) { + failure_count++; + XELOGW( + "{}: Copying from: {} to: {} failed! Error message: {} ({:08X})", + __func__, xe::path_to_utf8(title.path / title.name / "Headers"), + xe::path_to_utf8(common_path), ec.message(), ec.value()); + } + } + + if (!ec) { + // Remove previous directory + std::error_code ec; + std::filesystem::remove_all(title.path / title.name / "Headers", ec); + } + } + + if (std::filesystem::exists(title.path / title.name / "profile")) { + // Find directory with previous username. There should be only one! + const auto old_profile_data = + xe::filesystem::ListDirectories(title.path / title.name / "profile"); + + xe::filesystem::FileInfo& entry_to_copy = xe::filesystem::FileInfo(); + if (old_profile_data.size() != 1) { + for (const auto& entry : old_profile_data) { + if (entry.name == "User") { + entry_to_copy = entry; + } + } + } else { + entry_to_copy = old_profile_data.front(); + } + + const auto path_from = + title.path / title.name / "profile" / entry_to_copy.name; + std::error_code ec; + // Move files from inside to outside for convenience + std::filesystem::rename(path_from, path_to_profile_data / title.name, ec); + if (ec) { + failure_count++; + XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})", + __func__, xe::path_to_utf8(path_from), + xe::path_to_utf8(path_to_profile_data / title.name), + ec.message(), ec.value()); + } else { + std::error_code ec; + std::filesystem::remove_all(title.path / title.name / "profile", ec); + } + } + + const auto remaining_file_list = + xe::filesystem::ListDirectories(title.path / title.name); + + if (remaining_file_list.empty()) { + std::error_code ec; + std::filesystem::remove_all(title.path / title.name, ec); + } + } + + std::string migration_status_message = + fmt::format("Migration finished with {} {}.", failure_count, + failure_count == 1 ? "error" : "errors"); + + if (failure_count) { + migration_status_message.append( + " For more information check xenia.log file."); + } + new xe::ui::HostNotificationWindow(imgui_drawer_, "Migration Status", + migration_status_message, 0); + return X_STATUS_SUCCESS; +} + X_STATUS Emulator::InstallContentPackage( const std::filesystem::path& path, ContentInstallationInfo& installation_info) { @@ -613,16 +771,18 @@ X_STATUS Emulator::InstallContentPackage( (vfs::XContentContainerDevice*)device.get(); std::filesystem::path installation_path = - content_root() / fmt::format("{:08X}", dev->title_id()) / + content_root() / fmt::format("{:016X}", dev->xuid()) / + fmt::format("{:08X}", dev->title_id()) / fmt::format("{:08X}", dev->content_type()) / path.filename(); std::filesystem::path header_path = - content_root() / fmt::format("{:08X}", dev->title_id()) / "Headers" / + content_root() / fmt::format("{:016X}", dev->xuid()) / + fmt::format("{:08X}", dev->title_id()) / "Headers" / fmt::format("{:08X}", dev->content_type()) / path.filename(); installation_info.installation_path = - fmt::format("{:08X}/{:08X}/{}", dev->title_id(), dev->content_type(), - xe::path_to_utf8(path.filename())); + fmt::format("{:016X}/{:08X}/{:08X}/{}", dev->xuid(), dev->title_id(), + dev->content_type(), xe::path_to_utf8(path.filename())); installation_info.content_name = xe::to_utf8(dev->content_header().display_name()); diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 350e2ca02..ccf37d7f9 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -231,6 +231,9 @@ class Emulator { std::string content_name; }; + // Migrates data from content to content/xuid with respect to common data. + X_STATUS DataMigration(const uint64_t xuid); + // Extract content of package to content specific directory. X_STATUS InstallContentPackage(const std::filesystem::path& path, ContentInstallationInfo& installation_info); diff --git a/src/xenia/hid/input_system.cc b/src/xenia/hid/input_system.cc index 878efb9a5..e1a575ce7 100644 --- a/src/xenia/hid/input_system.cc +++ b/src/xenia/hid/input_system.cc @@ -14,6 +14,7 @@ #include "xenia/base/profiling.h" #include "xenia/hid/hid_flags.h" #include "xenia/hid/input_driver.h" +#include "xenia/kernel/util/shim_utils.h" namespace xe { namespace hid { @@ -39,7 +40,7 @@ void InputSystem::AddDriver(std::unique_ptr driver) { void InputSystem::UpdateUsedSlot(InputDriver* driver, uint8_t slot, bool connected) { - if (slot == 0xFF) { + if (slot == XUserIndexAny) { slot = 0; } @@ -50,6 +51,10 @@ void InputSystem::UpdateUsedSlot(InputDriver* driver, uint8_t slot, XELOGI(controller_slot_state_change_message[connected].c_str(), slot); connected_slots.flip(slot); + if (kernel::kernel_state()) { + kernel::kernel_state()->BroadcastNotification( + kXNotificationIDSystemInputDevicesChanged, 0); + } if (driver) { X_INPUT_CAPABILITIES capabilities = {}; @@ -147,15 +152,14 @@ void InputSystem::ToggleVibration() { // Send instant update to vibration state to prevent awaiting for next tick. X_INPUT_VIBRATION vibration = X_INPUT_VIBRATION(); - for (uint8_t user_index = 0; user_index < max_allowed_controllers; - user_index++) { + for (uint8_t user_index = 0; user_index < XUserMaxUserCount; user_index++) { SetState(user_index, &vibration); } } void InputSystem::AdjustDeadzoneLevels(const uint8_t slot, X_INPUT_GAMEPAD* gamepad) { - if (slot > max_allowed_controllers) { + if (slot > XUserMaxUserCount) { return; } diff --git a/src/xenia/hid/input_system.h b/src/xenia/hid/input_system.h index fc12e1f0b..fa6f5dbc5 100644 --- a/src/xenia/hid/input_system.h +++ b/src/xenia/hid/input_system.h @@ -27,8 +27,6 @@ class Window; namespace xe { namespace hid { -static constexpr uint8_t max_allowed_controllers = 4; - class InputSystem { public: explicit InputSystem(xe::ui::Window* window); @@ -51,7 +49,7 @@ class InputSystem { void ToggleVibration(); - const std::bitset GetConnectedSlots() const { + const std::bitset GetConnectedSlots() const { return connected_slots; } @@ -72,8 +70,8 @@ class InputSystem { std::vector> drivers_; - std::bitset connected_slots = {}; - std::array, max_allowed_controllers> + std::bitset connected_slots = {}; + std::array, XUserMaxUserCount> controllers_max_joystick_value = {}; xe_unlikely_mutex lock_; diff --git a/src/xenia/hid/sdl/sdl_input_driver.cc b/src/xenia/hid/sdl/sdl_input_driver.cc index cbbbd5f01..b04c5a3a0 100644 --- a/src/xenia/hid/sdl/sdl_input_driver.cc +++ b/src/xenia/hid/sdl/sdl_input_driver.cc @@ -282,7 +282,7 @@ X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags, // TODO(JoelLinn): Figure out the flags // https://github.com/evilC/UCR/blob/0489929e2a8e39caa3484c67f3993d3fba39e46f/Libraries/XInput.ahk#L85-L98 assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_); - bool user_any = users == 0xFF; + bool user_any = users == XUserIndexAny; if (users >= HID_SDL_USER_COUNT && !user_any) { return X_ERROR_BAD_ARGUMENTS; } diff --git a/src/xenia/hid/xinput/xinput_input_driver.cc b/src/xenia/hid/xinput/xinput_input_driver.cc index 7f1c9439e..62e659e1c 100644 --- a/src/xenia/hid/xinput/xinput_input_driver.cc +++ b/src/xenia/hid/xinput/xinput_input_driver.cc @@ -208,7 +208,7 @@ X_RESULT XInputInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, // we are not passing back an uninitialized X_INPUT_KEYSTROKE structure. // If any user (0xFF) is polled this bug does not occur but GetCapabilities // would fail so we need to skip it. - if (user_index != 0xFF) { + if (user_index != XUserIndexAny) { XINPUT_CAPABILITIES caps; auto xigc = (decltype(&XInputGetCapabilities))XInputGetCapabilities_; result = xigc(user_index, 0, &caps); diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 56f78db8d..47d978d39 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -106,6 +106,10 @@ KernelState::~KernelState() { KernelState* KernelState::shared() { return shared_kernel_state_; } uint32_t KernelState::title_id() const { + if (!executable_module_) { + return 0; + } + assert_not_null(executable_module_); xex2_opt_execution_info* exec_info = 0; @@ -577,7 +581,7 @@ std::vector KernelState::FindTitleUpdate( } return xam_state_->content_manager()->ListContent( - 1, xe::XContentType::kInstaller, title_id); + 1, 0, title_id, xe::XContentType::kInstaller); } const object_ref KernelState::LoadTitleUpdate( @@ -589,7 +593,7 @@ const object_ref KernelState::LoadTitleUpdate( } X_RESULT open_status = - content_manager()->OpenContent("UPDATE", *title_update, disc_number); + content_manager()->OpenContent("UPDATE", 0, *title_update, disc_number); // Use the corresponding patch for the launch module std::filesystem::path patch_xexp; diff --git a/src/xenia/kernel/xam/content_manager.cc b/src/xenia/kernel/xam/content_manager.cc index d12978e06..304d241ac 100644 --- a/src/xenia/kernel/xam/content_manager.cc +++ b/src/xenia/kernel/xam/content_manager.cc @@ -27,8 +27,6 @@ namespace kernel { namespace xam { static const char* kThumbnailFileName = "__thumbnail.png"; - -static const char* kGameUserContentDirName = "profile"; static const char* kGameContentHeaderDirName = "Headers"; static int content_device_id_ = 0; @@ -62,24 +60,28 @@ ContentManager::ContentManager(KernelState* kernel_state, ContentManager::~ContentManager() = default; std::filesystem::path ContentManager::ResolvePackageRoot( - XContentType content_type, uint32_t title_id) { + uint64_t xuid, uint32_t title_id, XContentType content_type) const { if (title_id == kCurrentlyRunningTitleId) { title_id = kernel_state_->title_id(); } + + auto xuid_str = fmt::format("{:016X}", xuid); auto title_id_str = fmt::format("{:08X}", title_id); - auto content_type_str = fmt::format("{:08X}", uint32_t(content_type)); + auto content_type_str = + fmt::format("{:08X}", static_cast(content_type)); // Package root path: // content_root/title_id/content_type/ - return root_path_ / title_id_str / content_type_str; + return root_path_ / xuid_str / title_id_str / content_type_str; } std::filesystem::path ContentManager::ResolvePackagePath( - const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number) { + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, + const uint32_t disc_number) { // Content path: // content_root/title_id/content_type/data_file_name/ auto get_package_path = [&, data, disc_number](const uint32_t title_id) { - auto package_root = ResolvePackageRoot(data.content_type, title_id); + auto package_root = ResolvePackageRoot(xuid, title_id, data.content_type); std::string final_name = xe::string_util::trim(data.file_name()); std::filesystem::path package_path = package_root / xe::to_path(final_name); @@ -91,7 +93,7 @@ std::filesystem::path ContentManager::ResolvePackagePath( if (data.content_type == XContentType::kPublisher) { const std::unordered_set title_ids = - FindPublisherTitleIds(data.title_id); + FindPublisherTitleIds(xuid, data.title_id); for (const auto& title_id : title_ids) { auto package_path = get_package_path(title_id); @@ -108,24 +110,30 @@ std::filesystem::path ContentManager::ResolvePackagePath( } std::filesystem::path ContentManager::ResolvePackageHeaderPath( - const std::string_view file_name, XContentType content_type, - uint32_t title_id) { + const std::string_view file_name, uint64_t xuid, uint32_t title_id, + const XContentType content_type) const { if (title_id == kCurrentlyRunningTitleId) { title_id = kernel_state_->title_id(); } + + if (content_type == XContentType::kMarketplaceContent) { + xuid = 0; + } + + auto xuid_str = fmt::format("{:016X}", xuid); auto title_id_str = fmt::format("{:08X}", title_id); auto content_type_str = fmt::format("{:08X}", uint32_t(content_type)); std::string final_name = xe::string_util::trim(std::string(file_name)) + ".header"; // Header root path: - // content_root/title_id/Headers/content_type/ - return root_path_ / title_id_str / kGameContentHeaderDirName / + // content_root/xuid/title_id/Headers/content_type/ + return root_path_ / xuid_str / title_id_str / kGameContentHeaderDirName / content_type_str / final_name; } std::unordered_set ContentManager::FindPublisherTitleIds( - uint32_t base_title_id) const { + const uint64_t xuid, uint32_t base_title_id) const { if (base_title_id == kCurrentlyRunningTitleId) { base_title_id = kernel_state_->title_id(); } @@ -134,9 +142,10 @@ std::unordered_set ContentManager::FindPublisherTitleIds( std::string publisher_id_regex = fmt::format("^{:04X}.*", static_cast(base_title_id >> 16)); // Get all publisher entries - auto publisher_entries = - xe::filesystem::FilterByName(xe::filesystem::ListDirectories(root_path_), - std::regex(publisher_id_regex)); + auto publisher_entries = xe::filesystem::FilterByName( + xe::filesystem::ListDirectories(root_path_ / + fmt::format("{:016X}", xuid)), + std::regex(publisher_id_regex)); for (const auto& entry : publisher_entries) { std::filesystem::path path_to_publisher_dir = @@ -159,23 +168,20 @@ std::unordered_set ContentManager::FindPublisherTitleIds( } std::vector ContentManager::ListContent( - uint32_t device_id, XContentType content_type, uint32_t title_id) { + const uint32_t device_id, const uint64_t xuid, const uint32_t title_id, + const XContentType content_type) const { std::vector result; - if (title_id == kCurrentlyRunningTitleId) { - title_id = kernel_state_->title_id(); - } - std::unordered_set title_ids = {title_id}; if (content_type == XContentType::kPublisher) { - title_ids = FindPublisherTitleIds(title_id); + title_ids = FindPublisherTitleIds(xuid, title_id); } for (const uint32_t& title_id : title_ids) { // Search path: - // content_root/title_id/type_name/* - auto package_root = ResolvePackageRoot(content_type, title_id); + // content_root/xuid/title_id/type_name/* + auto package_root = ResolvePackageRoot(xuid, title_id, content_type); auto file_infos = xe::filesystem::ListFiles(package_root); for (const auto& file_info : file_infos) { @@ -186,8 +192,8 @@ std::vector ContentManager::ListContent( XCONTENT_AGGREGATE_DATA content_data; if (XSUCCEEDED(ReadContentHeaderFile(xe::path_to_utf8(file_info.name), - content_type, content_data, - title_id))) { + xuid, title_id, content_type, + content_data))) { result.emplace_back(std::move(content_data)); } else { content_data.device_id = device_id; @@ -195,6 +201,7 @@ std::vector ContentManager::ListContent( content_data.set_display_name(xe::path_to_utf16(file_info.name)); content_data.set_file_name(xe::path_to_utf8(file_info.name)); content_data.title_id = title_id; + content_data.xuid = xuid; result.emplace_back(std::move(content_data)); } } @@ -203,9 +210,9 @@ std::vector ContentManager::ListContent( } std::unique_ptr ContentManager::ResolvePackage( - const std::string_view root_name, const XCONTENT_AGGREGATE_DATA& data, - const uint32_t disc_number) { - auto package_path = ResolvePackagePath(data, disc_number); + const std::string_view root_name, const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number) { + auto package_path = ResolvePackagePath(xuid, data, disc_number); if (!std::filesystem::exists(package_path)) { return nullptr; } @@ -217,15 +224,16 @@ std::unique_ptr ContentManager::ResolvePackage( return package; } -bool ContentManager::ContentExists(const XCONTENT_AGGREGATE_DATA& data) { - auto path = ResolvePackagePath(data); +bool ContentManager::ContentExists(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data) { + auto path = ResolvePackagePath(xuid, data); return std::filesystem::exists(path); } X_RESULT ContentManager::WriteContentHeaderFile( - const XCONTENT_AGGREGATE_DATA* data) { + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA* data) { auto header_path = ResolvePackageHeaderPath( - data->file_name(), data->content_type, data->title_id); + data->file_name(), xuid, data->title_id, data->content_type); auto parent_path = header_path.parent_path(); if (!std::filesystem::exists(parent_path)) { @@ -245,12 +253,12 @@ X_RESULT ContentManager::WriteContentHeaderFile( return X_STATUS_NO_SUCH_FILE; } -X_RESULT ContentManager::ReadContentHeaderFile(const std::string_view file_name, - XContentType content_type, - XCONTENT_AGGREGATE_DATA& data, - const uint32_t title_id) { +X_RESULT ContentManager::ReadContentHeaderFile( + const std::string_view file_name, const uint64_t xuid, + const uint32_t title_id, XContentType content_type, + XCONTENT_AGGREGATE_DATA& data) const { auto header_file_path = - ResolvePackageHeaderPath(file_name, content_type, title_id); + ResolvePackageHeaderPath(file_name, xuid, title_id, content_type); constexpr uint32_t header_size = sizeof(XCONTENT_AGGREGATE_DATA); if (std::filesystem::exists(header_file_path)) { @@ -275,15 +283,15 @@ X_RESULT ContentManager::ReadContentHeaderFile(const std::string_view file_name, // usually requires title_id to be provided // Kinda simple workaround for that, but still assumption data.title_id = title_id; - data.xuid = kernel_state_->xam_state() - ->GetUserProfile(static_cast(0)) - ->xuid(); + data.xuid = xuid; + return X_STATUS_SUCCESS; } return X_STATUS_NO_SUCH_FILE; } X_RESULT ContentManager::CreateContent(const std::string_view root_name, + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data) { auto global_lock = global_critical_region_.Acquire(); @@ -292,7 +300,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name, return X_ERROR_ALREADY_EXISTS; } - auto package_path = ResolvePackagePath(data); + auto package_path = ResolvePackagePath(xuid, data); if (std::filesystem::exists(package_path)) { // Exists, must not! return X_ERROR_ALREADY_EXISTS; @@ -302,7 +310,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name, return X_ERROR_ACCESS_DENIED; } - auto package = ResolvePackage(root_name, data); + auto package = ResolvePackage(root_name, xuid, data); assert_not_null(package); open_packages_.insert({string_key::create(root_name), package.release()}); @@ -311,6 +319,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name, } X_RESULT ContentManager::OpenContent(const std::string_view root_name, + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number) { auto global_lock = global_critical_region_.Acquire(); @@ -320,14 +329,14 @@ X_RESULT ContentManager::OpenContent(const std::string_view root_name, return X_ERROR_ALREADY_EXISTS; } - auto package_path = ResolvePackagePath(data, disc_number); + auto package_path = ResolvePackagePath(xuid, data, disc_number); if (!std::filesystem::exists(package_path)) { // Does not exist, must be created. return X_ERROR_FILE_NOT_FOUND; } // Open package. - auto package = ResolvePackage(root_name, data, disc_number); + auto package = ResolvePackage(root_name, xuid, data, disc_number); assert_not_null(package); open_packages_.insert({string_key::create(root_name), package.release()}); @@ -352,9 +361,11 @@ X_RESULT ContentManager::CloseContent(const std::string_view root_name) { } X_RESULT ContentManager::GetContentThumbnail( - const XCONTENT_AGGREGATE_DATA& data, std::vector* buffer) { + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, + std::vector* buffer) { auto global_lock = global_critical_region_.Acquire(); - auto package_path = ResolvePackagePath(data); + + auto package_path = ResolvePackagePath(xuid, data); auto thumb_path = package_path / kThumbnailFileName; if (std::filesystem::exists(thumb_path)) { auto file = xe::filesystem::OpenFile(thumb_path, "rb"); @@ -369,9 +380,10 @@ X_RESULT ContentManager::GetContentThumbnail( } X_RESULT ContentManager::SetContentThumbnail( - const XCONTENT_AGGREGATE_DATA& data, std::vector buffer) { + const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, + std::vector buffer) { auto global_lock = global_critical_region_.Acquire(); - auto package_path = ResolvePackagePath(data); + auto package_path = ResolvePackagePath(xuid, data); std::filesystem::create_directories(package_path); if (std::filesystem::exists(package_path)) { auto thumb_path = package_path / kThumbnailFileName; @@ -384,7 +396,8 @@ X_RESULT ContentManager::SetContentThumbnail( } } -X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) { +X_RESULT ContentManager::DeleteContent(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data) { auto global_lock = global_critical_region_.Acquire(); if (IsContentOpen(data)) { @@ -392,7 +405,7 @@ X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) { return X_ERROR_ACCESS_DENIED; } - auto package_path = ResolvePackagePath(data); + auto package_path = ResolvePackagePath(xuid, data); if (std::filesystem::remove_all(package_path) > 0) { return X_ERROR_SUCCESS; } else { @@ -400,15 +413,13 @@ X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) { } } -std::filesystem::path ContentManager::ResolveGameUserContentPath() { +std::filesystem::path ContentManager::ResolveGameUserContentPath( + const uint64_t xuid) { + auto xuid_str = fmt::format("{:016X}", xuid); auto title_id = fmt::format("{:08X}", kernel_state_->title_id()); - auto user_name = xe::to_path(kernel_state_->xam_state() - ->GetUserProfile(static_cast(0)) - ->name()); - // Per-game per-profile data location: - // content_root/title_id/profile/user_name - return root_path_ / title_id / kGameUserContentDirName / user_name; + return root_path_ / xuid_str / kDashboardStringID / "00010000" / xuid_str / + title_id; } bool ContentManager::IsContentOpen(const XCONTENT_AGGREGATE_DATA& data) const { diff --git a/src/xenia/kernel/xam/content_manager.h b/src/xenia/kernel/xam/content_manager.h index 826aeec56..f7d104eda 100644 --- a/src/xenia/kernel/xam/content_manager.h +++ b/src/xenia/kernel/xam/content_manager.h @@ -142,45 +142,52 @@ class ContentManager { const std::filesystem::path& root_path); ~ContentManager(); - std::vector ListContent(uint32_t device_id, - XContentType content_type, - uint32_t title_id = -1); + std::vector ListContent( + const uint32_t device_id, const uint64_t xuid, const uint32_t title_id, + const XContentType content_type) const; std::unique_ptr ResolvePackage( - const std::string_view root_name, const XCONTENT_AGGREGATE_DATA& data, - const uint32_t disc_number = -1); + const std::string_view root_name, const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number = -1); - bool ContentExists(const XCONTENT_AGGREGATE_DATA& data); - X_RESULT WriteContentHeaderFile(const XCONTENT_AGGREGATE_DATA* data_raw); + bool ContentExists(const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data); + X_RESULT WriteContentHeaderFile(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA* data_raw); X_RESULT ReadContentHeaderFile(const std::string_view file_name, + const uint64_t xuid, const uint32_t title_id, XContentType content_type, - XCONTENT_AGGREGATE_DATA& data, - const uint32_t title_id = -1); - X_RESULT CreateContent(const std::string_view root_name, + XCONTENT_AGGREGATE_DATA& data) const; + X_RESULT CreateContent(const std::string_view root_name, const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data); - X_RESULT OpenContent(const std::string_view root_name, + X_RESULT OpenContent(const std::string_view root_name, const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number = -1); X_RESULT CloseContent(const std::string_view root_name); - X_RESULT GetContentThumbnail(const XCONTENT_AGGREGATE_DATA& data, + X_RESULT GetContentThumbnail(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data, std::vector* buffer); - X_RESULT SetContentThumbnail(const XCONTENT_AGGREGATE_DATA& data, + X_RESULT SetContentThumbnail(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data, std::vector buffer); - X_RESULT DeleteContent(const XCONTENT_AGGREGATE_DATA& data); - std::filesystem::path ResolveGameUserContentPath(); + X_RESULT DeleteContent(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data); + std::filesystem::path ResolveGameUserContentPath(const uint64_t xuid); bool IsContentOpen(const XCONTENT_AGGREGATE_DATA& data) const; void CloseOpenedFilesFromContent(const std::string_view root_name); private: - std::filesystem::path ResolvePackageRoot(XContentType content_type, - uint32_t title_id = -1); - std::filesystem::path ResolvePackagePath(const XCONTENT_AGGREGATE_DATA& data, + std::filesystem::path ResolvePackageRoot( + const uint64_t xuid, const uint32_t title_id, + const XContentType content_type) const; + std::filesystem::path ResolvePackagePath(const uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number = -1); std::filesystem::path ResolvePackageHeaderPath( - const std::string_view file_name, XContentType content_type, - uint32_t title_id = -1); + const std::string_view file_name, uint64_t xuid, uint32_t title_id, + const XContentType content_type) const; std::unordered_set FindPublisherTitleIds( + const uint64_t xuid, uint32_t base_title_id = kCurrentlyRunningTitleId) const; KernelState* kernel_state_; diff --git a/src/xenia/kernel/xam/profile_manager.cc b/src/xenia/kernel/xam/profile_manager.cc new file mode 100644 index 000000000..34d2d55db --- /dev/null +++ b/src/xenia/kernel/xam/profile_manager.cc @@ -0,0 +1,534 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/profile_manager.h" + +#include +#include + +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/base/logging.h" +#include "xenia/emulator.h" +#include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/crypto_utils.h" +#include "xenia/vfs/devices/host_path_device.h" + +DEFINE_string(logged_profile_slot_0_xuid, "", + "XUID of the profile to load on boot in slot 0", "Profiles"); +DEFINE_string(logged_profile_slot_1_xuid, "", + "XUID of the profile to load on boot in slot 1", "Profiles"); +DEFINE_string(logged_profile_slot_2_xuid, "", + "XUID of the profile to load on boot in slot 2", "Profiles"); +DEFINE_string(logged_profile_slot_3_xuid, "", + "XUID of the profile to load on boot in slot 3", "Profiles"); + +namespace xe { +namespace kernel { +namespace xam { + +bool ProfileManager::DecryptAccountFile(const uint8_t* data, + X_XAMACCOUNTINFO* output, bool devkit) { + const uint8_t* key = util::GetXeKey(0x19, devkit); + if (!key) { + return false; // this shouldn't happen... + } + + // Generate RC4 key from data hash + uint8_t rc4_key[0x14]; + util::HmacSha(key, 0x10, data, 0x10, 0, 0, 0, 0, rc4_key, 0x14); + + uint8_t dec_data[sizeof(X_XAMACCOUNTINFO) + 8]; + + // Decrypt data + util::RC4(rc4_key, 0x10, data + 0x10, sizeof(dec_data), dec_data, + sizeof(dec_data)); + + // Verify decrypted data against hash + uint8_t data_hash[0x14]; + util::HmacSha(key, 0x10, dec_data, sizeof(dec_data), 0, 0, 0, 0, data_hash, + 0x14); + + if (std::memcmp(data, data_hash, 0x10) == 0) { + // Copy account data to output + std::memcpy(output, dec_data + 8, sizeof(X_XAMACCOUNTINFO)); + + // Swap gamertag endian + xe::copy_and_swap(output->gamertag, output->gamertag, 0x10); + return true; + } + + return false; +} + +void ProfileManager::EncryptAccountFile(const X_XAMACCOUNTINFO* input, + uint8_t* output, bool devkit) { + const uint8_t* key = util::GetXeKey(0x19, devkit); + if (!key) { + return; // this shouldn't happen... + } + + X_XAMACCOUNTINFO* output_acct = + reinterpret_cast(output + 0x18); + std::memcpy(output_acct, input, sizeof(X_XAMACCOUNTINFO)); + + // Swap gamertag endian + xe::copy_and_swap(output_acct->gamertag, output_acct->gamertag, + 0x10); + + // Set confounder, should be random but meh + std::memset(output + 0x10, 0xFD, 8); + + // Encrypted data = xam account info + 8 byte confounder + uint32_t enc_data_size = sizeof(X_XAMACCOUNTINFO) + 8; + + // Set data hash + uint8_t data_hash[0x14]; + util::HmacSha(key, 0x10, output + 0x10, enc_data_size, 0, 0, 0, 0, data_hash, + 0x14); + + std::memcpy(output, data_hash, 0x10); + + // Generate RC4 key from data hash + uint8_t rc4_key[0x14]; + util::HmacSha(key, 0x10, data_hash, 0x10, 0, 0, 0, 0, rc4_key, 0x14); + + // Encrypt data + util::RC4(rc4_key, 0x10, output + 0x10, enc_data_size, output + 0x10, + enc_data_size); +} + +ProfileManager::ProfileManager(KernelState* kernel_state) + : kernel_state_(kernel_state) { + logged_profiles_.clear(); + accounts_.clear(); + + LoadAccounts(FindProfiles()); + + if (!cvars::logged_profile_slot_0_xuid.empty()) { + Login(xe::string_util::from_string( + cvars::logged_profile_slot_0_xuid, true), + 0); + } + + if (!cvars::logged_profile_slot_1_xuid.empty()) { + Login(xe::string_util::from_string( + cvars::logged_profile_slot_1_xuid, true), + 1); + } + + if (!cvars::logged_profile_slot_2_xuid.empty()) { + Login(xe::string_util::from_string( + cvars::logged_profile_slot_2_xuid, true), + 2); + } + + if (!cvars::logged_profile_slot_3_xuid.empty()) { + Login(xe::string_util::from_string( + cvars::logged_profile_slot_3_xuid, true), + 3); + } +} + +ProfileManager::~ProfileManager() {} + +void ProfileManager::ReloadProfiles() { LoadAccounts(FindProfiles()); } + +UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const { + const uint8_t user_index = GetUserIndexAssignedToProfile(xuid); + if (user_index >= XUserMaxUserCount) { + return nullptr; + } + + return GetProfile(user_index); +} + +UserProfile* ProfileManager::GetProfile(uint8_t user_index) const { + if (user_index == XUserIndexNone) { + return nullptr; + } + + if (user_index == XUserIndexAny) { + for (uint8_t i = 0; i < XUserMaxUserCount; i++) { + if (!logged_profiles_.count(i)) { + continue; + } + + return logged_profiles_.at(i).get(); + } + } + + if (!logged_profiles_.count(user_index)) { + return nullptr; + } + + return logged_profiles_.at(user_index).get(); +} + +bool ProfileManager::LoadAccount(const uint64_t xuid) { + const std::string xuid_as_string = fmt::format("{:016X}", xuid); + + XELOGI("{}: Loading Account: {}", __func__, xuid_as_string); + + MountProfile(xuid); + + const std::string guest_path = xuid_as_string + ":\\Account"; + + xe::vfs::File* output_file; + xe::vfs::FileAction action = {}; + auto status = kernel_state_->file_system()->OpenFile( + nullptr, guest_path, xe::vfs::FileDisposition::kOpen, + xe::vfs::FileAccess::kFileReadData, false, true, &output_file, &action); + + if (XFAILED(status) || !output_file || !output_file->entry()) { + XELOGI("{}: Failed to open Account file: {:08X}", __func__, status); + DismountProfile(xuid); + return false; + } + + std::vector file_data; + file_data.resize(output_file->entry()->size()); + + size_t bytes_read = 0; + output_file->ReadSync(file_data.data(), output_file->entry()->size(), 0, + &bytes_read); + output_file->Destroy(); + + if (bytes_read < sizeof(X_XAMACCOUNTINFO)) { + DismountProfile(xuid); + return false; + } + + X_XAMACCOUNTINFO tmp_acct; + if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct)) { + if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct, + true)) { + XELOGW("Failed to decrypt account data file for XUID: {}", + xuid_as_string); + return false; + } + } + + // We don't need profile to be mounted anymore, so for now let's close it. + // We need it only when we want to login into this account! + DismountProfile(xuid); + + accounts_.insert({xuid, tmp_acct}); + return true; +} + +void ProfileManager::LoadAccounts(const std::vector profiles_xuids) { + for (const auto& path : profiles_xuids) { + LoadAccount(path); + } +} + +bool ProfileManager::MountProfile(const uint64_t xuid) { + std::filesystem::path profile_path = GetProfilePath(xuid); + std::string mount_path = fmt::format("{:016X}", xuid) + ':'; + + auto device = + std::make_unique(mount_path, profile_path, false); + if (!device->Initialize()) { + XELOGE( + "MountProfile: Unable to mount {} profile; file not found or " + "corrupted.", + xe::path_to_utf8(profile_path)); + return false; + } + return kernel_state_->file_system()->RegisterDevice(std::move(device)); +} + +bool ProfileManager::DismountProfile(const uint64_t xuid) { + return kernel_state_->file_system()->UnregisterDevice( + fmt::format("{:016X}", xuid) + ':'); +} + +void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index) { + if (logged_profiles_.size() >= 4 && user_index >= XUserMaxUserCount) { + XELOGE( + "Cannot login account with XUID: {:016X} due to lack of free slots " + "(Max 4 accounts at once)", + xuid); + return; + } + + if (user_index < XUserMaxUserCount) { + const auto& profile = logged_profiles_.find(user_index); + if (profile != logged_profiles_.cend()) { + if (profile->second && profile->second->xuid() == xuid) { + // Do nothing! User is already signed in to that slot. + return; + } + } + } + + // Find if xuid is already logged in. We might want to logout. + for (auto& logged_profile : logged_profiles_) { + if (logged_profile.second->xuid() == xuid) { + Logout(logged_profile.first); + } + } + + if (!accounts_.count(xuid)) { + return; + } + + auto& profile = accounts_[xuid]; + const uint8_t assigned_user_slot = + user_index < XUserMaxUserCount ? user_index : FindFirstFreeProfileSlot(); + + XELOGI("Loaded {} (GUID: {:016X}) to slot {}", profile.GetGamertagString(), + xuid, assigned_user_slot); + + logged_profiles_[assigned_user_slot] = + std::make_unique(xuid, &profile); + kernel_state_->BroadcastNotification(kXNotificationIDSystemSignInChanged, + GetUsedUserSlots().to_ulong()); + UpdateConfig(xuid, assigned_user_slot); +} + +void ProfileManager::Logout(const uint8_t user_index) { + auto profile = logged_profiles_.find(user_index); + if (profile == logged_profiles_.cend()) { + return; + } + DismountProfile(profile->second->xuid()); + logged_profiles_.erase(profile); + kernel_state_->BroadcastNotification(kXNotificationIDSystemSignInChanged, + GetUsedUserSlots().to_ulong()); + UpdateConfig(0, user_index); +} + +std::vector ProfileManager::FindProfiles() const { + // Info: Profile directory name is also it's offline xuid + std::vector profiles_xuids; + + auto profiles_directory = xe::filesystem::FilterByName( + xe::filesystem::ListDirectories( + kernel_state_->emulator()->content_root()), + std::regex("[0-9A-F]{16}")); + + for (const auto& profile : profiles_directory) { + const std::string profile_xuid = xe::path_to_utf8(profile.name); + if (profile_xuid == fmt::format("{:016X}", 0)) { + continue; + } + + if (!std::filesystem::exists( + profile.path / profile.name / kDashboardStringID / + fmt::format("{:08X}", XContentType::kProfile) / profile.name)) { + XELOGE("Profile {} doesn't have profile package!", profile_xuid); + continue; + } + + XELOGE("{}: Adding profile {} to profile list", __func__, profile_xuid); + profiles_xuids.push_back( + xe::string_util::from_string(profile_xuid, true)); + } + + XELOGE("ProfileManager: Found {} Profiles", profiles_xuids.size()); + return profiles_xuids; +} + +uint8_t ProfileManager::FindFirstFreeProfileSlot() const { + if (!IsAnyProfileSignedIn()) { + return 0; + } + + std::bitset used_slots = {}; + for (const auto& [index, entry] : logged_profiles_) { + used_slots.set(index); + } + + for (uint8_t i = 0; i < used_slots.size(); ++i) { + if (!used_slots[i]) { + return i; + } + } + return -1; +} + +std::bitset ProfileManager::GetUsedUserSlots() const { + std::bitset used_slots = {}; + for (const auto& [index, entry] : logged_profiles_) { + if (!entry) { + continue; + } + + used_slots.set(index); + } + + return used_slots; +} + +uint8_t ProfileManager::GetUserIndexAssignedToProfile( + const uint64_t xuid) const { + for (const auto& [index, entry] : logged_profiles_) { + if (!entry) { + continue; + } + + if (entry->xuid() != xuid) { + continue; + } + + return index; + } + return -1; +} + +std::filesystem::path ProfileManager::GetProfileContentPath( + const uint64_t xuid, const uint32_t title_id) const { + std::filesystem::path profile_content_path = + kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid); + if (title_id != -1 && title_id != 0) { + profile_content_path = + profile_content_path / fmt::format("{:08X}", title_id); + } + return profile_content_path; +} + +std::filesystem::path ProfileManager::GetProfilePath( + const uint64_t xuid) const { + return GetProfilePath(fmt::format("{:016X}", xuid)); +} + +std::filesystem::path ProfileManager::GetProfilePath( + const std::string xuid) const { + return kernel_state_->emulator()->content_root() / xuid / kDashboardStringID / + fmt::format("{:08X}", XContentType::kProfile) / xuid; +} + +bool ProfileManager::CreateProfile(const std::string gamertag, + bool default_xuid) { + const auto xuid = !default_xuid ? GenerateXuid() : 0xB13EBABEBABEBABE; + + if (!std::filesystem::create_directories(GetProfilePath(xuid))) { + return false; + } + + if (!MountProfile(xuid)) { + return false; + } + + const bool is_account_created = CreateAccount(xuid, gamertag); + if (is_account_created && default_xuid) { + Login(xuid); + } + return is_account_created; +} + +bool ProfileManager::CreateAccount(const uint64_t xuid, + const std::string gamertag) { + const std::string guest_path = + xe::string_util::to_hex_string(xuid) + ":\\Account"; + + xe::vfs::File* output_file; + xe::vfs::FileAction action = {}; + auto status = kernel_state_->file_system()->OpenFile( + nullptr, guest_path, xe::vfs::FileDisposition::kCreate, + xe::vfs::FileAccess::kFileWriteData, false, true, &output_file, &action); + + if (XFAILED(status) || !output_file || !output_file->entry()) { + XELOGI("{}: Failed to open Account file for creation: {:08X}", __func__, + status); + DismountProfile(xuid); + return false; + } + + X_XAMACCOUNTINFO account = {}; + std::u16string gamertag_u16 = xe::to_utf16(gamertag); + + string_util::copy_truncating(account.gamertag, gamertag_u16, + sizeof(account.gamertag)); + + std::vector encrypted_data; + encrypted_data.resize(sizeof(X_XAMACCOUNTINFO) + 0x18); + EncryptAccountFile(&account, encrypted_data.data()); + + size_t written_bytes = 0; + output_file->WriteSync(encrypted_data.data(), encrypted_data.size(), 0, + &written_bytes); + output_file->Destroy(); + + DismountProfile(xuid); + + accounts_.insert({xuid, account}); + return true; +} + +void ProfileManager::UpdateConfig(const uint64_t xuid, const uint8_t slot) { + if (slot >= XUserMaxUserCount) { + return; + } + + const std::string hex_xuid = xe::string_util::to_hex_string(xuid); + switch (slot) { + case 0: + OVERRIDE_string(logged_profile_slot_0_xuid, hex_xuid); + break; + case 1: + OVERRIDE_string(logged_profile_slot_1_xuid, hex_xuid); + break; + case 2: + OVERRIDE_string(logged_profile_slot_2_xuid, hex_xuid); + break; + case 3: + OVERRIDE_string(logged_profile_slot_3_xuid, hex_xuid); + break; + default: + break; + } + return; +} + +bool ProfileManager::DeleteProfile(const uint64_t xuid) { + const uint8_t user_index = GetUserIndexAssignedToProfile(xuid); + + if (user_index < XUserMaxUserCount) { + Logout(user_index); + } + + DismountProfile(xuid); + + if (accounts_.count(xuid)) { + accounts_.erase(xuid); + } + + std::error_code ec; + std::filesystem::remove_all(GetProfileContentPath(xuid), ec); + if (ec) { + XELOGE("Cannot remove profile: {}", ec.message()); + return false; + } + return true; +} + +bool ProfileManager::IsGamertagValid(const std::string gamertag) { + if (gamertag.empty()) { + return false; + } + + if (gamertag.length() > 15) { + return false; + } + + // Gamertag cannot start with a number. + if (std::isdigit(gamertag.at(0))) { + return false; + } + + return std::find_if(gamertag.cbegin(), gamertag.cend(), [](char c) { + return !(std::isalnum(c) || (c == ' ')); + }) == gamertag.cend(); +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/profile_manager.h b/src/xenia/kernel/xam/profile_manager.h new file mode 100644 index 000000000..cb326b3a8 --- /dev/null +++ b/src/xenia/kernel/xam/profile_manager.h @@ -0,0 +1,142 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_PROFILE_MANAGER_H_ +#define XENIA_KERNEL_XAM_PROFILE_MANAGER_H_ + +#include +#include +#include +#include + +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/base/string.h" +#include "xenia/kernel/xam/user_profile.h" +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +class KernelState; +} // namespace kernel +} // namespace xe + +namespace xe { +namespace kernel { +namespace xam { + +constexpr uint32_t kDashboardID = 0xFFFE07D1; +const static std::string kDashboardStringID = + fmt::format("{:08X}", kDashboardID); + +enum class XTileType { + kAchievement, + kGameIcon, + kGamerTile, + kGamerTileSmall, + kLocalGamerTile, + kLocalGamerTileSmall, + kBkgnd, + kAwardedGamerTile, + kAwardedGamerTileSmall, + kGamerTileByImageId, + kPersonalGamerTile, + kPersonalGamerTileSmall, + kGamerTileByKey, + kAvatarGamerTile, + kAvatarGamerTileSmall, + kAvatarFullBody +}; + +// TODO: find filenames of other tile types that are stored in profile +static const std::map kTileFileNames = { + {XTileType::kPersonalGamerTile, "tile_64.png"}, + {XTileType::kPersonalGamerTileSmall, "tile_32.png"}, + {XTileType::kAvatarGamerTile, "avtr_64.png"}, + {XTileType::kAvatarGamerTileSmall, "avtr_32.png"}, +}; + +class ProfileManager { + public: + static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output, + bool devkit = false); + + static void EncryptAccountFile(const X_XAMACCOUNTINFO* input, uint8_t* output, + bool devkit = false); + + // Profile: + // - Account + // - GPDs (Dashboard, titles) + + // Loading Profile means load everything + // Loading Account means load basic data + ProfileManager(KernelState* kernel_state); + + ~ProfileManager(); + + bool CreateProfile(const std::string gamertag, bool default_xuid = false); + // bool CreateProfile(const X_XAMACCOUNTINFO* account_info); + bool DeleteProfile(const uint64_t xuid); + + bool MountProfile(const uint64_t xuid); + bool DismountProfile(const uint64_t xuid); + + void Login(const uint64_t xuid, const uint8_t user_index = -1); + void Logout(const uint8_t user_index); + + bool LoadAccount(const uint64_t xuid); + void LoadAccounts(const std::vector profiles_xuids); + + void ReloadProfiles(); + + UserProfile* GetProfile(const uint64_t xuid) const; + UserProfile* GetProfile(const uint8_t user_index) const; + uint8_t GetUserIndexAssignedToProfile(const uint64_t xuid) const; + + std::map* GetProfiles() { return &accounts_; } + + uint32_t GetProfilesCount() const { + return static_cast(accounts_.size()); + } + bool IsAnyProfileSignedIn() const { return !logged_profiles_.empty(); } + + std::filesystem::path GetProfileContentPath( + const uint64_t xuid, const uint32_t title_id = -1) const; + + 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); + + std::filesystem::path GetProfilePath(const uint64_t xuid) const; + std::filesystem::path GetProfilePath(const std::string xuid) const; + + std::vector FindProfiles() const; + + uint8_t FindFirstFreeProfileSlot() const; + std::bitset GetUsedUserSlots() const; + + uint64_t GenerateXuid() const { + std::random_device rd; + std::mt19937 gen(rd()); + + return ((uint64_t)0xE03 << 52) + (gen() % (1 << 31)); + } + + std::map accounts_; + std::map> logged_profiles_; + + KernelState* kernel_state_; +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_PROFILE_MANAGER_H_ diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index d2a69e49f..726ac0af0 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -19,15 +19,11 @@ namespace xe { namespace kernel { namespace xam { -UserProfile::UserProfile(uint8_t index) { +UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info) + : xuid_(xuid), account_info_(*account_info) { // 58410A1F checks the user XUID against a mask of 0x00C0000000000000 (3<<54), // if non-zero, it prevents the user from playing the game. // "You do not have permissions to perform this operation." - xuid_ = 0xB13EBABEBABEBABE + index; - name_ = "User"; - if (index) { - name_ = "User_" + std::to_string(index); - } // https://cs.rin.ru/forum/viewtopic.php?f=38&t=60668&hilit=gfwl+live&start=195 // https://github.com/arkem/py360/blob/master/py360/constants.py @@ -136,7 +132,7 @@ UserSetting* UserProfile::GetSetting(uint32_t setting_id) { void UserProfile::LoadSetting(UserSetting* setting) { if (setting->is_title_specific()) { const std::filesystem::path content_dir = - kernel_state()->content_manager()->ResolveGameUserContentPath(); + kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_); const std::string setting_id_str = fmt::format("{:08X}", setting->GetSettingId()); const std::filesystem::path file_path = content_dir / setting_id_str; @@ -182,7 +178,7 @@ void UserProfile::SaveSetting(UserSetting* setting) { if (setting->is_title_specific() && setting->GetSettingSource() == X_USER_PROFILE_SETTING_SOURCE::TITLE) { const std::filesystem::path content_dir = - kernel_state()->content_manager()->ResolveGameUserContentPath(); + kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_); std::filesystem::create_directories(content_dir); diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index b68d89fc8..7b940e6f0 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -153,10 +153,10 @@ class UserSetting { class UserProfile { public: - UserProfile(uint8_t index); + UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info); uint64_t xuid() const { return xuid_; } - std::string name() const { return name_; } + std::string name() const { return account_info_.GetGamertagString(); } uint32_t signin_state() const { return 1; } uint32_t type() const { return 1 | 2; /* local | online profile? */ } @@ -170,7 +170,8 @@ class UserProfile { private: uint64_t xuid_; - std::string name_; + X_XAMACCOUNTINFO account_info_; + std::vector> setting_list_; std::unordered_map settings_; diff --git a/src/xenia/kernel/xam/xam_content.cc b/src/xenia/kernel/xam/xam_content.cc index 7bbed096d..76b5c8536 100644 --- a/src/xenia/kernel/xam/xam_content.cc +++ b/src/xenia/kernel/xam/xam_content.cc @@ -92,28 +92,66 @@ dword_result_t XamContentCreateEnumerator_entry( *buffer_size_ptr = sizeof(XCONTENT_DATA) * items_per_enumerate; } + uint64_t xuid = 0; + if (user_index != XUserIndexNone) { + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + + xuid = user->xuid(); + } + auto e = make_object>(kernel_state(), items_per_enumerate); - auto result = e->Initialize(0xFF, 0xFE, 0x20005, 0x20007, 0); + auto result = e->Initialize(XUserIndexAny, 0xFE, 0x20005, 0x20007, 0); if (XFAILED(result)) { return result; } + std::vector enumerated_content = {}; + if (!device_info || device_info->device_id == DummyDeviceId::HDD) { - // Get all content data. - auto content_datas = kernel_state()->content_manager()->ListContent( - static_cast(DummyDeviceId::HDD), - XContentType(uint32_t(content_type))); - for (const auto& content_data : content_datas) { - auto item = e->AppendItem(); - *item = content_data; + if (xuid) { + auto user_enumerated_data = + kernel_state()->content_manager()->ListContent( + static_cast(DummyDeviceId::HDD), xuid, + kernel_state()->title_id(), XContentType(uint32_t(content_type))); + + enumerated_content.insert(enumerated_content.end(), + user_enumerated_data.cbegin(), + user_enumerated_data.cend()); } + + if (!(content_flags & 0x00001000)) { + auto common_enumerated_data = + kernel_state()->content_manager()->ListContent( + static_cast(DummyDeviceId::HDD), 0, + kernel_state()->title_id(), XContentType(uint32_t(content_type))); + + enumerated_content.insert(enumerated_content.end(), + common_enumerated_data.cbegin(), + common_enumerated_data.cend()); + } + + // Remove duplicates + enumerated_content.erase( + std::unique(enumerated_content.begin(), enumerated_content.end()), + enumerated_content.end()); } if (!device_info || device_info->device_id == DummyDeviceId::ODD) { // TODO(gibbed): disc drive content } + for (const auto& content_data : enumerated_content) { + auto item = e->AppendItem(); + *item = content_data; + XELOGI("{}: Adding: {} (Filename: {}) to enumerator result", __func__, + xe::to_utf8(content_data.display_name()), content_data.file_name()); + } + XELOGD("XamContentCreateEnumerator: added {} items to enumerator", e->item_count()); @@ -131,6 +169,17 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, lpdword_t license_mask_ptr, dword_t cache_size, qword_t content_size, lpvoid_t overlapped_ptr) { + uint64_t xuid = 0; + if (user_index != XUserIndexNone) { + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + + xuid = user->xuid(); + } + XCONTENT_AGGREGATE_DATA content_data; if (content_data_size == sizeof(XCONTENT_DATA)) { content_data = *content_data_ptr.as(); @@ -141,13 +190,17 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, return X_ERROR_INVALID_PARAMETER; } + if (content_data.content_type == XContentType::kMarketplaceContent) { + xuid = 0; + } + auto content_manager = kernel_state()->content_manager(); if (overlapped_ptr && disposition_ptr) { *disposition_ptr = 0; } - auto run = [content_manager, root_name = root_name.value(), flags, + auto run = [content_manager, xuid, root_name = root_name.value(), flags, content_data, disposition_ptr, license_mask_ptr, overlapped_ptr]( uint32_t& extended_error, uint32_t& length) -> X_RESULT { X_RESULT result = X_ERROR_INVALID_PARAMETER; @@ -155,7 +208,7 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, switch (flags & 0xF) { case 1: // CREATE_NEW // Fail if exists. - if (content_manager->ContentExists(content_data)) { + if (content_manager->ContentExists(xuid, content_data)) { result = X_ERROR_ALREADY_EXISTS; } else { disposition = kDispositionState::Create; @@ -163,14 +216,14 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, break; case 2: // CREATE_ALWAYS // Overwrite existing, if any. - if (content_manager->ContentExists(content_data)) { - content_manager->DeleteContent(content_data); + if (content_manager->ContentExists(xuid, content_data)) { + content_manager->DeleteContent(xuid, content_data); } disposition = kDispositionState::Create; break; case 3: // OPEN_EXISTING // Open only if exists. - if (!content_manager->ContentExists(content_data)) { + if (!content_manager->ContentExists(xuid, content_data)) { result = X_ERROR_PATH_NOT_FOUND; } else { disposition = kDispositionState::Open; @@ -178,7 +231,7 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, break; case 4: // OPEN_ALWAYS // Create if needed. - if (!content_manager->ContentExists(content_data)) { + if (!content_manager->ContentExists(xuid, content_data)) { disposition = kDispositionState::Create; } else { disposition = kDispositionState::Open; @@ -186,10 +239,10 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, break; case 5: // TRUNCATE_EXISTING // Fail if doesn't exist, if does exist delete and recreate. - if (!content_manager->ContentExists(content_data)) { + if (!content_manager->ContentExists(xuid, content_data)) { result = X_ERROR_PATH_NOT_FOUND; } else { - content_manager->DeleteContent(content_data); + content_manager->DeleteContent(xuid, content_data); disposition = kDispositionState::Create; } break; @@ -199,12 +252,12 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, } if (disposition == kDispositionState::Create) { - result = content_manager->CreateContent(root_name, content_data); + result = content_manager->CreateContent(root_name, xuid, content_data); if (XSUCCEEDED(result)) { - content_manager->WriteContentHeaderFile(&content_data); + content_manager->WriteContentHeaderFile(xuid, &content_data); } } else if (disposition == kDispositionState::Open) { - result = content_manager->OpenContent(root_name, content_data); + result = content_manager->OpenContent(root_name, xuid, content_data); } if (license_mask_ptr && XSUCCEEDED(result)) { @@ -265,7 +318,7 @@ dword_result_t XamContentCreateInternal_entry( lpstring_t root_name, lpvoid_t content_data_ptr, dword_t flags, lpdword_t disposition_ptr, lpdword_t license_mask_ptr, dword_t cache_size, qword_t content_size, lpvoid_t overlapped_ptr) { - return xeXamContentCreate(0xFE, root_name, content_data_ptr, + return xeXamContentCreate(XUserIndexNone, root_name, content_data_ptr, sizeof(XCONTENT_AGGREGATE_DATA), flags, disposition_ptr, license_mask_ptr, cache_size, content_size, overlapped_ptr); @@ -319,15 +372,21 @@ dword_result_t XamContentGetCreator_entry(dword_t user_index, return X_ERROR_INVALID_PARAMETER; } + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as(); - auto run = [content_data, user_index, is_creator_ptr, creator_xuid_ptr, - overlapped_ptr](uint32_t& extended_error, - uint32_t& length) -> X_RESULT { + auto run = [content_data, xuid = user->xuid(), user_index, is_creator_ptr, + creator_xuid_ptr, overlapped_ptr](uint32_t& extended_error, + uint32_t& length) -> X_RESULT { X_RESULT result = X_ERROR_SUCCESS; bool content_exists = - kernel_state()->content_manager()->ContentExists(content_data); + kernel_state()->content_manager()->ContentExists(xuid, content_data); if (content_exists) { if (content_data.content_type == XContentType::kSavedGame) { @@ -377,6 +436,12 @@ dword_result_t XamContentGetThumbnail_entry(dword_t user_index, lpvoid_t buffer_ptr, lpdword_t buffer_size_ptr, lpunknown_t overlapped_ptr) { + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + assert_not_null(buffer_size_ptr); uint32_t buffer_size = *buffer_size_ptr; XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as(); @@ -384,7 +449,7 @@ dword_result_t XamContentGetThumbnail_entry(dword_t user_index, // Get thumbnail (if it exists). std::vector buffer; auto result = kernel_state()->content_manager()->GetContentThumbnail( - content_data, &buffer); + user->xuid(), content_data, &buffer); *buffer_size_ptr = uint32_t(buffer.size()); @@ -416,13 +481,19 @@ dword_result_t XamContentSetThumbnail_entry(dword_t user_index, lpvoid_t buffer_ptr, dword_t buffer_size, lpunknown_t overlapped_ptr) { + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as(); // Buffer is PNG data. auto buffer = std::vector((uint8_t*)buffer_ptr, (uint8_t*)buffer_ptr + buffer_size); auto result = kernel_state()->content_manager()->SetContentThumbnail( - content_data, std::move(buffer)); + user->xuid(), content_data, std::move(buffer)); if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result); @@ -436,9 +507,20 @@ DECLARE_XAM_EXPORT1(XamContentSetThumbnail, kContent, kImplemented); dword_result_t XamContentDelete_entry(dword_t user_index, lpvoid_t content_data_ptr, lpunknown_t overlapped_ptr) { + if (user_index >= XUserMaxUserCount) { + return X_ERROR_ACCESS_DENIED; + } + + const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user) { + return X_ERROR_NO_SUCH_USER; + } + XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as(); - auto result = kernel_state()->content_manager()->DeleteContent(content_data); + auto result = kernel_state()->content_manager()->DeleteContent(user->xuid(), + content_data); if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result); @@ -453,7 +535,8 @@ dword_result_t XamContentDeleteInternal_entry(lpvoid_t content_data_ptr, lpunknown_t overlapped_ptr) { // INFO: Analysis of xam.xex shows that "internal" functions are wrappers with // 0xFE as user_index - return XamContentDelete_entry(0xFE, content_data_ptr, overlapped_ptr); + return XamContentDelete_entry(XUserIndexNone, content_data_ptr, + overlapped_ptr); } DECLARE_XAM_EXPORT1(XamContentDeleteInternal, kContent, kImplemented); diff --git a/src/xenia/kernel/xam/xam_content_aggregate.cc b/src/xenia/kernel/xam/xam_content_aggregate.cc index eb41c29f7..c8dce92f0 100644 --- a/src/xenia/kernel/xam/xam_content_aggregate.cc +++ b/src/xenia/kernel/xam/xam_content_aggregate.cc @@ -91,7 +91,7 @@ dword_result_t XamContentAggregateCreateEnumerator_entry(qword_t xuid, auto e = make_object>( kernel_state(), 1); X_KENUMERATOR_CONTENT_AGGREGATE* extra; - auto result = e->Initialize(0xFF, 0xFE, 0x2000E, 0x20010, 0, &extra); + auto result = e->Initialize(XUserIndexAny, 0xFE, 0x2000E, 0x20010, 0, &extra); if (XFAILED(result)) { return result; } @@ -115,8 +115,8 @@ dword_result_t XamContentAggregateCreateEnumerator_entry(qword_t xuid, for (auto& title_id : title_ids) { // Get all content data. auto content_datas = kernel_state()->content_manager()->ListContent( - static_cast(DummyDeviceId::HDD), content_type_enum, - title_id); + static_cast(DummyDeviceId::HDD), xuid == -1 ? 0 : xuid, + title_id, content_type_enum); for (const auto& content_data : content_datas) { auto item = e->AppendItem(); assert_not_null(item); diff --git a/src/xenia/kernel/xam/xam_content_device.cc b/src/xenia/kernel/xam/xam_content_device.cc index f223b5f3c..10b24522c 100644 --- a/src/xenia/kernel/xam/xam_content_device.cc +++ b/src/xenia/kernel/xam/xam_content_device.cc @@ -159,7 +159,7 @@ dword_result_t XamContentCreateDeviceEnumerator_entry(dword_t content_type, auto e = make_object>(kernel_state(), max_count); - auto result = e->Initialize(0xFE, 0xFE, 0x2000A, 0x20009, 0); + auto result = e->Initialize(XUserIndexNone, 0xFE, 0x2000A, 0x20009, 0); if (XFAILED(result)) { return result; } diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 2acb24ea3..41b20d98b 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -32,7 +32,7 @@ constexpr uint32_t XINPUT_FLAG_ANYDEVICE = 0xFF; constexpr uint32_t XINPUT_FLAG_ANY_USER = 1 << 30; dword_result_t XAutomationpUnbindController_entry(dword_t user_index) { - if (user_index > 4) { + if (user_index >= XUserMaxUserCount) { return 0; } @@ -79,7 +79,8 @@ dword_result_t XamInputGetCapabilitiesEx_entry( } uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) { + if ((actual_user_index & XUserIndexAny) == XUserIndexAny || + (flags & XINPUT_FLAG_ANY_USER)) { // Always pin user to 0. actual_user_index = 0; } @@ -105,7 +106,7 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags, if (input_state) { memset((void*)input_state.host_address(), 0, sizeof(X_INPUT_STATE)); } - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_DEVICE_NOT_CONNECTED; } @@ -118,7 +119,8 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags, uint32_t actual_user_index = user_index; // chrispy: change this, logic is not right - if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) { + if ((actual_user_index & XUserIndexAny) == XUserIndexAny || + (flags & XINPUT_FLAG_ANY_USER)) { // Always pin user to 0. actual_user_index = 0; } @@ -134,19 +136,13 @@ dword_result_t XamInputSetState_entry( dword_t user_index, dword_t flags, /* flags, as far as i can see, is not used*/ pointer_t vibration) { - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_E_DEVICE_NOT_CONNECTED; } if (!vibration) { return X_ERROR_BAD_ARGUMENTS; } - uint32_t actual_user_index = user_index; - if ((user_index & 0xFF) == 0xFF) { - // Always pin user to 0. - actual_user_index = 0; - } - auto input_system = kernel_state()->emulator()->input_system(); auto lock = input_system->lock(); return input_system->SetState(user_index, vibration); @@ -170,7 +166,8 @@ dword_result_t XamInputGetKeystroke_entry( } uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) { + if ((actual_user_index & XUserIndexAny) == XUserIndexAny || + (flags & XINPUT_FLAG_ANY_USER)) { // Always pin user to 0. actual_user_index = 0; } @@ -197,7 +194,7 @@ dword_result_t XamInputGetKeystrokeEx_entry( uint32_t user_index = *user_index_ptr; auto input_system = kernel_state()->emulator()->input_system(); auto lock = input_system->lock(); - if ((user_index & 0xFF) == 0xFF) { + if ((user_index & XUserIndexAny) == XUserIndexAny) { // Always pin user to 0. user_index = 0; } @@ -206,7 +203,7 @@ dword_result_t XamInputGetKeystrokeEx_entry( // That flag means we should iterate over every connected controller and // check which one have pending request. auto result = X_ERROR_DEVICE_NOT_CONNECTED; - for (uint32_t i = 0; i < 4; i++) { + for (uint32_t i = 0; i < XUserMaxUserCount; i++) { auto result = input_system->GetKeystroke(i, flags, keystroke); // Return result from first user that have pending request @@ -235,7 +232,7 @@ X_HRESULT_result_t XamUserGetDeviceContext_entry(dword_t user_index, // set zero just to be safe. *out_ptr = 0; if (kernel_state()->xam_state()->IsUserSignedIn(user_index) || - (user_index & 0xFF) == 0xFF) { + (user_index & XUserIndexAny) == XUserIndexAny) { *out_ptr = (uint32_t)user_index; return X_E_SUCCESS; } else { diff --git a/src/xenia/kernel/xam/xam_state.cc b/src/xenia/kernel/xam/xam_state.cc index 97cb5c6ee..7f7b2c956 100644 --- a/src/xenia/kernel/xam/xam_state.cc +++ b/src/xenia/kernel/xam/xam_state.cc @@ -10,10 +10,6 @@ #include "xenia/kernel/xam/xam_state.h" #include "xenia/emulator.h" -DEFINE_uint32(max_signed_profiles, 4, - "Limits how many profiles can be assigned. Possible values: 1-4", - "Kernel"); - namespace xe { namespace kernel { namespace xam { @@ -29,8 +25,7 @@ XamState::XamState(Emulator* emulator, KernelState* kernel_state) content_manager_ = std::make_unique(kernel_state, content_root); - user_profiles_.emplace(0, std::make_unique(0)); - + profile_manager_ = std::make_unique(kernel_state); achievement_manager_ = std::make_unique(); AppManager::RegisterApps(kernel_state, app_manager_.get()); @@ -43,47 +38,20 @@ XamState::~XamState() { } UserProfile* XamState::GetUserProfile(uint32_t user_index) const { - if (!IsUserSignedIn(user_index)) { + if (user_index >= XUserMaxUserCount && user_index < XUserIndexLatest) { return nullptr; } - return user_profiles_.at(user_index).get(); + + return profile_manager_->GetProfile(static_cast(user_index)); } UserProfile* XamState::GetUserProfile(uint64_t xuid) const { - for (const auto& [key, value] : user_profiles_) { - if (value->xuid() == xuid) { - return user_profiles_.at(key).get(); - } - } - return nullptr; -} - -void XamState::UpdateUsedUserProfiles() { - const std::bitset<4> used_slots = kernel_state_->GetConnectedUsers(); - - const uint32_t signed_profile_count = - std::max(static_cast(1), - std::min(static_cast(4), cvars::max_signed_profiles)); - - for (uint32_t i = 1; i < signed_profile_count; i++) { - bool is_used = used_slots.test(i); - - if (IsUserSignedIn(i) && !is_used) { - user_profiles_.erase(i); - kernel_state_->BroadcastNotification( - kXNotificationIDSystemInputDevicesChanged, 0); - } - - if (!IsUserSignedIn(i) && is_used) { - user_profiles_.emplace(i, std::make_unique(i)); - kernel_state_->BroadcastNotification( - kXNotificationIDSystemInputDevicesChanged, 0); - } - } + return profile_manager_->GetProfile(xuid); } bool XamState::IsUserSignedIn(uint32_t user_index) const { - return user_profiles_.find(user_index) != user_profiles_.cend(); + return profile_manager_->GetProfile(static_cast(user_index)) != + nullptr; } bool XamState::IsUserSignedIn(uint64_t xuid) const { diff --git a/src/xenia/kernel/xam/xam_state.h b/src/xenia/kernel/xam/xam_state.h index f93dec08c..4c0526bc4 100644 --- a/src/xenia/kernel/xam/xam_state.h +++ b/src/xenia/kernel/xam/xam_state.h @@ -14,7 +14,7 @@ #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/app_manager.h" #include "xenia/kernel/xam/content_manager.h" -#include "xenia/kernel/xam/user_profile.h" +#include "xenia/kernel/xam/profile_manager.h" namespace xe { class Emulator; @@ -40,12 +40,11 @@ class XamState { AchievementManager* achievement_manager() const { return achievement_manager_.get(); } + ProfileManager* profile_manager() const { return profile_manager_.get(); } UserProfile* GetUserProfile(uint32_t user_index) const; UserProfile* GetUserProfile(uint64_t xuid) const; - void UpdateUsedUserProfiles(); - bool IsUserSignedIn(uint32_t user_index) const; bool IsUserSignedIn(uint64_t xuid) const; @@ -55,8 +54,7 @@ class XamState { std::unique_ptr app_manager_; std::unique_ptr content_manager_; std::unique_ptr achievement_manager_; - - std::map> user_profiles_; + std::unique_ptr profile_manager_; }; } // namespace xam diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index 6fc6a8058..4b57ad1e1 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -565,13 +565,13 @@ dword_result_t XamShowDeviceSelectorUI_entry( return X_ERROR_INVALID_PARAMETER; } - if ((user_index >= 4 && user_index != 0xFF) || + if ((user_index >= XUserMaxUserCount && user_index != XUserIndexAny) || (content_flags & 0x83F00008) != 0 || !device_id_ptr) { XOverlappedSetExtendedError(overlapped, X_ERROR_INVALID_PARAMETER); return X_ERROR_INVALID_PARAMETER; } - if (user_index != 0xFF && + if (user_index != XUserIndexAny && !kernel_state()->xam_state()->IsUserSignedIn(user_index)) { kernel_state()->CompleteOverlappedImmediate(overlapped, X_ERROR_NO_SUCH_USER); @@ -676,7 +676,7 @@ dword_result_t XamShowMarketplaceUI_entry(dword_t user_index, dword_t ui_type, // 1 - view content specified by offer id // content_types: // game specific, usually just -1 - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } @@ -752,7 +752,7 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry( // ui_type: // 1000 - free // 1001 - paid - if (user_index >= 4 || !offers || num_offers > 6) { + if (user_index >= XUserMaxUserCount || !offers || num_offers > 6) { return X_ERROR_INVALID_PARAMETER; } @@ -819,6 +819,39 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry( } DECLARE_XAM_EXPORT1(XamShowMarketplaceDownloadItemsUI, kUI, kSketchy); +dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t unk_mask) { + // XN_SYS_UI (on) + kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 1); + // Mask values vary. Probably matching user types? Local/remote? + // Games seem to sit and loop until we trigger this notification: + + auto run = [users_needed]() -> void { + uint32_t user_mask = 0; + uint32_t active_users = 0; + + for (uint32_t i = 0; i < XUserMaxUserCount; i++) { + if (kernel_state()->xam_state()->IsUserSignedIn(i)) { + user_mask |= (1 << i); + active_users++; + if (active_users >= users_needed) break; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + // XN_SYS_SIGNINCHANGED (players) + kernel_state()->BroadcastNotification(kXNotificationIDSystemSignInChanged, + user_mask); + // XN_SYS_UI (off) + kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 0); + }; + + std::thread thread(run); + thread.detach(); + + return X_ERROR_SUCCESS; +} +DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kStub); + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 3099af6f3..e51d7618e 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -33,25 +33,32 @@ X_HRESULT_result_t XamUserGetXUID_entry(dword_t user_index, dword_t type_mask, if (!xuid_ptr) { return X_E_INVALIDARG; } + + *xuid_ptr = 0; + + if (user_index >= XUserMaxUserCount) { + return X_E_INVALIDARG; + } + + if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { + return X_E_NO_SUCH_USER; + } + + const auto& user_profile = + kernel_state()->xam_state()->GetUserProfile(user_index); + uint32_t result = X_E_NO_SUCH_USER; uint64_t xuid = 0; - if (user_index < 4) { - if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { - const auto& user_profile = - kernel_state()->xam_state()->GetUserProfile(user_index); - auto type = user_profile->type() & type_mask; - if (type & (2 | 4)) { - // maybe online profile? - xuid = user_profile->xuid(); - result = X_E_SUCCESS; - } else if (type & 1) { - // maybe offline profile? - xuid = user_profile->xuid(); - result = X_E_SUCCESS; - } - } - } else { - result = X_E_INVALIDARG; + + auto type = user_profile->type() & type_mask; + if (type & (2 | 4)) { + // maybe online profile? + xuid = user_profile->xuid(); + result = X_E_SUCCESS; + } else if (type & 1) { + // maybe offline profile? + xuid = user_profile->xuid(); + result = X_E_SUCCESS; } *xuid_ptr = xuid; return result; @@ -62,12 +69,14 @@ dword_result_t XamUserGetSigninState_entry(dword_t user_index) { // Yield, as some games spam this. xe::threading::MaybeYield(); uint32_t signin_state = 0; - if (user_index < 4) { - if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { - const auto& user_profile = - kernel_state()->xam_state()->GetUserProfile(user_index); - signin_state = user_profile->signin_state(); - } + if (user_index >= XUserMaxUserCount) { + return signin_state; + } + + if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { + const auto& user_profile = + kernel_state()->xam_state()->GetUserProfile(user_index); + signin_state = user_profile->signin_state(); } return signin_state; } @@ -91,12 +100,10 @@ X_HRESULT_result_t XamUserGetSigninInfo_entry( } std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO)); - if (user_index > 3) { + if (user_index >= XUserMaxUserCount) { return X_E_NO_SUCH_USER; } - kernel_state()->xam_state()->UpdateUsedUserProfiles(); - if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); @@ -113,7 +120,7 @@ DECLARE_XAM_EXPORT1(XamUserGetSigninInfo, kUserProfiles, kImplemented); dword_result_t XamUserGetName_entry(dword_t user_index, lpstring_t buffer, dword_t buffer_len) { - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } @@ -134,7 +141,7 @@ DECLARE_XAM_EXPORT1(XamUserGetName, kUserProfiles, kImplemented); dword_result_t XamUserGetGamerTag_entry(dword_t user_index, lpu16string_t buffer, dword_t buffer_len) { - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_E_INVALIDARG; } @@ -231,9 +238,9 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, return X_ERROR_INSUFFICIENT_BUFFER; } - // Title ID = 0 means us. - // 0xfffe07d1 = profile? - if (!kernel_state()->xam_state()->IsUserSignedIn(user_index) && !xuids) { + auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); + + if (!user_profile && !xuids) { if (overlapped) { kernel_state()->CompleteOverlappedImmediate( kernel_state()->memory()->HostToGuestVirtual(overlapped), @@ -243,8 +250,6 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, return X_ERROR_NO_SUCH_USER; } - auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); - if (xuids) { uint64_t user_xuid = static_cast(xuids[0]); if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) { @@ -259,6 +264,10 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid); } + if (!user_profile) { + return X_ERROR_NO_SUCH_USER; + } + // First call asks for size (fill buffer_size_ptr). // Second call asks for buffer contents with that size. @@ -355,9 +364,12 @@ dword_result_t XamUserWriteProfileSettings_entry( if (!setting_count || !settings) { return X_ERROR_INVALID_PARAMETER; } + // Update and save settings. + const auto& user_profile = + kernel_state()->xam_state()->GetUserProfile(user_index); // Skip writing data about users with id != 0 they're not supported - if (user_index > 0) { + if (!user_profile) { if (overlapped) { kernel_state()->CompleteOverlappedImmediate( kernel_state()->memory()->HostToGuestVirtual(overlapped), @@ -366,9 +378,6 @@ dword_result_t XamUserWriteProfileSettings_entry( } return X_ERROR_SUCCESS; } - // Update and save settings. - const auto& user_profile = - kernel_state()->xam_state()->GetUserProfile(user_index); for (uint32_t n = 0; n < setting_count; ++n) { const X_USER_PROFILE_SETTING& setting = settings[n]; @@ -431,8 +440,8 @@ DECLARE_XAM_EXPORT1(XamUserWriteProfileSettings, kUserProfiles, kImplemented); dword_result_t XamUserCheckPrivilege_entry(dword_t user_index, dword_t mask, lpdword_t out_value) { // checking all users? - if (user_index != 0xFF) { - if (user_index >= 4) { + if (user_index != XUserIndexAny) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } @@ -494,7 +503,7 @@ dword_result_t XamUserIsOnlineEnabled_entry(dword_t user_index) { return 1; } DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub); dword_result_t XamUserGetMembershipTier_entry(dword_t user_index) { - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } @@ -511,7 +520,7 @@ dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1, uint32_t are_friends = 0; X_RESULT result; - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { result = X_ERROR_INVALID_PARAMETER; } else { if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { @@ -550,40 +559,6 @@ dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1, } DECLARE_XAM_EXPORT1(XamUserAreUsersFriends, kUserProfiles, kStub); -dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t unk_mask) { - // XN_SYS_UI (on) - kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 1); - kernel_state()->xam_state()->UpdateUsedUserProfiles(); - // Mask values vary. Probably matching user types? Local/remote? - // Games seem to sit and loop until we trigger this notification: - - auto run = [users_needed]() -> void { - uint32_t user_mask = 0; - uint32_t active_users = 0; - - for (uint32_t i = 0; i < 4; i++) { - if (kernel_state()->xam_state()->IsUserSignedIn(i)) { - user_mask |= (1 << i); - active_users++; - if (active_users >= users_needed) break; - } - } - - std::this_thread::sleep_for(std::chrono::milliseconds(150)); - // XN_SYS_SIGNINCHANGED (players) - kernel_state()->BroadcastNotification(kXNotificationIDSystemSignInChanged, - user_mask); - // XN_SYS_UI (off) - kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 0); - }; - - std::thread thread(run); - thread.detach(); - - return X_ERROR_SUCCESS; -} -DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kStub); - dword_result_t XamUserCreateAchievementEnumerator_entry( dword_t title_id, dword_t user_index, dword_t xuid, dword_t flags, dword_t offset, dword_t count, lpdword_t buffer_size_ptr, @@ -592,7 +567,7 @@ dword_result_t XamUserCreateAchievementEnumerator_entry( return X_ERROR_INVALID_PARAMETER; } - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } @@ -724,7 +699,7 @@ dword_result_t XamUserGetSubscriptionType_entry(dword_t user_index, dword_t unk2, dword_t unk3, dword_t unk4, dword_t unk5, dword_t unk6) { - if (!unk2 || !unk3 || user_index > 4) { + if (!unk2 || !unk3 || user_index >= XUserMaxUserCount) { return X_E_INVALIDARG; } @@ -749,7 +724,7 @@ dword_result_t XamUserCreateStatsEnumerator_entry( return X_ERROR_INVALID_PARAMETER; } - if (user_index >= 4) { + if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } diff --git a/src/xenia/vfs/devices/stfs_xbox.h b/src/xenia/vfs/devices/stfs_xbox.h index 4312ddd77..cc63c0c34 100644 --- a/src/xenia/vfs/devices/stfs_xbox.h +++ b/src/xenia/vfs/devices/stfs_xbox.h @@ -340,7 +340,8 @@ struct XContentMetadata { } description_ex_raw; std::u16string display_name(XLanguage language) const { - uint32_t lang_id = uint32_t(language) - 1; + uint32_t lang_id = + language == XLanguage::kInvalid ? 1 : uint32_t(language) - 1; if (lang_id >= kNumLanguagesV2) { assert_always(); diff --git a/src/xenia/vfs/devices/xcontent_container_device.h b/src/xenia/vfs/devices/xcontent_container_device.h index 2623ac3ed..928364693 100644 --- a/src/xenia/vfs/devices/xcontent_container_device.h +++ b/src/xenia/vfs/devices/xcontent_container_device.h @@ -53,6 +53,8 @@ class XContentContainerDevice : public Device { return files_total_size_ - sizeof(XContentContainerHeader); } + uint64_t xuid() const { return header_->content_metadata.profile_id; } + uint32_t title_id() const { return header_->content_metadata.execution_info.title_id; } diff --git a/src/xenia/xbox.h b/src/xenia/xbox.h index b8f96dd22..ddcdb99dd 100644 --- a/src/xenia/xbox.h +++ b/src/xenia/xbox.h @@ -14,6 +14,7 @@ #include #include "xenia/base/memory.h" +#include "xenia/base/string.h" // TODO(benvanik): split this header, cleanup, etc. // clang-format off @@ -259,6 +260,12 @@ struct X_UNICODE_STRING { }; static_assert_size(X_UNICODE_STRING, 8); +constexpr uint8_t XUserMaxUserCount = 4; + +constexpr uint8_t XUserIndexLatest = 0xFD; +constexpr uint8_t XUserIndexNone = 0xFE; +constexpr uint8_t XUserIndexAny = 0xFF; + // https://github.com/ThirteenAG/Ultimate-ASI-Loader/blob/master/source/xlive/xliveless.h typedef uint32_t XNotificationID; enum : XNotificationID { @@ -539,6 +546,84 @@ enum class XDeploymentType : uint32_t { kUnknown = 0xFF, }; +#pragma pack(push, 4) +struct X_XAMACCOUNTINFO { + enum AccountReservedFlags { + kPasswordProtected = 0x10000000, + kLiveEnabled = 0x20000000, + kRecovering = 0x40000000, + kVersionMask = 0x000000FF + }; + + enum AccountUserFlags { + kPaymentInstrumentCreditCard = 1, + + kCountryMask = 0xFF00, + kSubscriptionTierMask = 0xF00000, + kLanguageMask = 0x3E000000, + + kParentalControlEnabled = 0x1000000, + }; + + enum AccountSubscriptionTier { + kSubscriptionTierSilver = 3, + kSubscriptionTierGold = 6, + kSubscriptionTierFamilyGold = 9 + }; + + enum AccountLiveFlags { kAcctRequiresManagement = 1 }; + + xe::be reserved_flags; + xe::be live_flags; + char16_t gamertag[0x10]; + xe::be xuid_online; // 09.... + xe::be cached_user_flags; + xe::be network_id; + char passcode[4]; + char online_domain[0x14]; + char online_kerberos_realm[0x18]; + char online_key[0x10]; + char passport_membername[0x72]; + char passport_password[0x20]; + char owner_passport_membername[0x72]; + + bool IsPasscodeEnabled() { + return static_cast(reserved_flags & + AccountReservedFlags::kPasswordProtected); + } + + bool IsLiveEnabled() { + return static_cast(reserved_flags & + AccountReservedFlags::kLiveEnabled); + } + + bool IsXUIDOffline() { return ((xuid_online >> 60) & 0xF) == 0xE; } + bool IsXUIDOnline() { return ((xuid_online >> 48) & 0xFFFF) == 0x9; } + bool IsXUIDValid() { return IsXUIDOffline() != IsXUIDOnline(); } + bool IsTeamXUID() { + return (xuid_online & 0xFF00000000000140) == 0xFE00000000000100; + } + + uint32_t GetCountry() const { + return (cached_user_flags & kCountryMask) >> 8; + } + + AccountSubscriptionTier GetSubscriptionTier() const { + return static_cast( + (cached_user_flags & kSubscriptionTierMask) >> 20); + } + + XLanguage GetLanguage() const { + return static_cast((cached_user_flags & kLanguageMask) >> 25); + } + + std::string GetGamertagString() const { + return xe::to_utf8(std::u16string(gamertag)); + } +}; +static_assert_size(X_XAMACCOUNTINFO, 0x17C); +#pragma pack(pop) + } // namespace xe // clang-format on