From 2e93a23a5ef61d228be8f8570bb6919a8402f993 Mon Sep 17 00:00:00 2001 From: emoose Date: Fri, 3 Jan 2020 00:05:00 +0000 Subject: [PATCH] [XAM/User] Add support for multiple signed-in users/profiles This adds support for multiple profiles to Xenia, profiles can be configured with the [Profiles] user_*_xuid / user_*_state config settings. If state is non-zero (1 = offline, 2 = LIVE), the profile will be counted as logged-on - either with a generated XeniaUser gamertag, or if the XUID is set to 1 the first available profile will be loaded. The XUID can also be set to the offline-XUID (E000...) of an existing profile, to sign in the user as that profile. (Profiles should be stored in the Xenia content/FFFE07D1/00010000/ folder, either as an STFS package or an extracted folder) All the XamUser* functions have been updated to support multiple user_index's provided to them too. (there's still issues with weird indexes like 0xFF, 0x7FF9... being given though, still dunno what's with that, the KernelState::user_profile() code will treat 0xFF as 0) I'm not really sure if this is the most ideal way to do things though, but it does appear to work fine, at least Halo 3 does detect the profiles with state > 0 fine. TODO: look into changing up xam_content to make use of user_index & profiles. It shouldn't be too difficult now to emulate the same content paths X360 uses (seperating content by XUID etc) Would probably be a good idea, since it'd probably be needed for us to support multiple profiles properly, so that they don't all share savegames etc... --- src/xenia/emulator.cc | 8 +- src/xenia/kernel/kernel_state.cc | 2 +- src/xenia/kernel/kernel_state.h | 40 +++- src/xenia/kernel/xam/apps/xgi_app.cc | 23 +- src/xenia/kernel/xam/user_profile.cc | 345 ++++++++++++++++++++------- src/xenia/kernel/xam/user_profile.h | 33 ++- src/xenia/kernel/xam/xam_content.cc | 24 +- src/xenia/kernel/xam/xam_user.cc | 184 +++++++------- 8 files changed, 450 insertions(+), 209 deletions(-) diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index b3edd18c5..fcaa94de1 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -686,7 +686,13 @@ X_STATUS Emulator::CompleteLaunch(const std::wstring& path, if (spa.Read(module->memory()->TranslateVirtual(resource_data), resource_size)) { // Set title SPA and get title name/icon - kernel_state_->user_profile()->SetTitleSpaData(spa); + for (uint32_t i = 0; i < kernel_state_->num_profiles(); i++) { + auto profile = kernel_state_->user_profile(i); + if (profile) { + profile->SetTitleSpaData(spa); + } + } + game_title_ = xe::to_wstring(spa.GetTitleName()); auto icon_block = spa.GetIcon(); if (icon_block) { diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 1293a16c6..fc4ce72cb 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -53,7 +53,7 @@ KernelState::KernelState(Emulator* emulator) content_root = xe::to_absolute_path(content_root); content_manager_ = std::make_unique(this, content_root); - user_profile_ = std::make_unique(this); + xam::UserProfile::CreateUsers(this, user_profiles_); assert_null(shared_kernel_state_); shared_kernel_state_ = this; diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 95ce08fb9..bdfbdd92e 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -102,7 +102,42 @@ class KernelState { xam::ContentManager* content_manager() const { return content_manager_.get(); } - xam::UserProfile* user_profile() const { return user_profile_.get(); } + + // Returns pointer to UserProfile for the given user_index + // Returns nullptr if user_index is invalid, or user isn't signed in + // (unless allow_signed_out is true, to allow for code dealing with sign-ins + // etc) + xam::UserProfile* user_profile(uint32_t user_index, + bool allow_signed_out = false) const { + if (user_index == 0xFF) { + user_index = 0; + } + + if (user_index >= xam::kMaxNumUsers) { + return nullptr; + } + + auto user = &user_profiles_[user_index]; + if (!allow_signed_out) { + if (!user->get()->signed_in()) { + return nullptr; + } + } + return user->get(); + } + + uint32_t num_profiles() const { return xam::kMaxNumUsers; } + + xam::UserProfile* user_profile(uint64_t xuid) const { + for (int i = 0; i < xam::kMaxNumUsers; i++) { + auto profile = user_profiles_[i].get(); + if (profile->xuid() == xuid || profile->xuid_offline() == xuid) { + return profile; + } + } + + return nullptr; + } // Access must be guarded by the global critical region. util::ObjectTable* object_table() { return &object_table_; } @@ -188,7 +223,8 @@ class KernelState { std::unique_ptr app_manager_; std::unique_ptr content_manager_; - std::unique_ptr user_profile_; + + std::unique_ptr user_profiles_[xam::kMaxNumUsers]; xe::global_critical_region global_critical_region_; diff --git a/src/xenia/kernel/xam/apps/xgi_app.cc b/src/xenia/kernel/xam/apps/xgi_app.cc index 88fcd573c..8f43463d7 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -61,17 +61,21 @@ X_RESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, XELOGD("XGIUserWriteAchievements(%.8X, %.8X)", achievement_count, achievements_ptr); - auto* game_gpd = kernel_state_->user_profile()->GetTitleGpd(); - if (!game_gpd) { - XELOGE("XGIUserWriteAchievements failed, no game GPD set?"); - return X_ERROR_SUCCESS; - } - bool modified = false; auto* achievement = (X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr); xdbf::Achievement ach; for (uint32_t i = 0; i < achievement_count; i++, achievement++) { + auto user_profile = kernel_state_->user_profile(achievement->user_idx); + if (!user_profile) { + continue; + } + + auto game_gpd = user_profile->GetTitleGpd(); + if (!game_gpd) { + continue; + } + if (game_gpd->GetAchievement(achievement->achievement_id, &ach)) { if (!ach.IsUnlocked()) { XELOGI("Achievement Unlocked! %ws (%d gamerscore) - %ws", @@ -81,9 +85,10 @@ X_RESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, modified = true; } } - } - if (modified) { - kernel_state_->user_profile()->UpdateTitleGpd(); + + if (modified) { + user_profile->UpdateTitleGpd(); + } } return X_ERROR_SUCCESS; diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 132b66097..da0f6f43f 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -24,12 +24,138 @@ DECLARE_int32(license_mask); -DEFINE_int64(profile_xuid, -1, "XUID of the profile to load in (E0...)", "XAM"); +DEFINE_string(user_0_xuid, "", "XUID of the profile to use for user 0 (E0...", + "Profiles"); + +DEFINE_int32(user_0_state, 1, "User 0 signin state (0/1/2)", "Profiles"); + +DEFINE_string(user_1_xuid, "", "XUID of the profile to use for user 1 (E0...", + "Profiles"); + +DEFINE_int32(user_1_state, 0, "User 1 signin state (0/1/2)", "Profiles"); + +DEFINE_string(user_2_xuid, "", "XUID of the profile to use for user 2 (E0...", + "Profiles"); + +DEFINE_int32(user_2_state, 0, "User 2 signin state (0/1/2)", "Profiles"); + +DEFINE_string(user_3_xuid, "", "XUID of the profile to use for user 3 (E0...", + "Profiles"); + +DEFINE_int32(user_3_state, 0, "User 3 signin state (0/1/2)", "Profiles"); namespace xe { namespace kernel { namespace xam { +void UserProfile::CreateUsers(KernelState* kernel_state, + std::unique_ptr* profiles) { + std::string xuids[] = {cvars::user_0_xuid, cvars::user_1_xuid, + cvars::user_2_xuid, cvars::user_3_xuid}; + int32_t states[] = {cvars::user_0_state, cvars::user_1_state, + cvars::user_2_state, cvars::user_3_state}; + + // Create UserProfile instances for all 4 user slots + // But only login the slots the player has already configured + for (int i = 0; i < kMaxNumUsers; i++) { + auto xuid_str = xuids[i]; + int32_t state = states[i]; + + uint64_t xuid = 0; + if (!xuid_str.empty()) { + xuid = std::strtoull(xuid_str.c_str(), 0, 16); + } + + auto profile = std::make_unique(kernel_state); + profile->signin_state(state); + if (state > 0) { + profile->Login(xuid); + } + profiles[i] = std::move(profile); + } +} + +uint64_t UserProfile::XuidFromPath(const std::wstring& path) { + filesystem::FileInfo info; + if (!filesystem::GetInfo(path, &info)) { + return -1; + } + + auto fname = info.name; + size_t len = fname.length(); + + // Get substring of name that has valid hex chars + // TODO: atm this just stops at . if the filename has it.. + // it should probably check each char is valid hex instead + size_t i = 0; + for (auto c : fname) { + if (c == '.') { + len = i; + break; + } + i++; + } + + auto xuid_str = xe::to_string(fname.substr(0, len)); + return std::strtoull(xuid_str.c_str(), 0, 16); +} + +std::wstring UserProfile::base_path(KernelState* state) { + return xe::to_absolute_path( + state->content_manager()->ResolvePackageRoot(0x10000, 0xFFFE07D1)); +} + +std::map> +UserProfile::Enumerate(KernelState* state, bool exclude_signed_in) { + std::map> map; + + auto files = filesystem::ListFiles(base_path(state)); + for (auto f : files) { + auto profile_path = f.path + f.name; + // use .dir for this profile if it exists + auto dir_path = profile_path + L".dir\\"; + if (filesystem::PathExists(dir_path)) { + profile_path = dir_path; + } + + auto xuid = XuidFromPath(profile_path); + if (!xuid) { + continue; // could cause infinite loop, so skip any invalid ones + } + if (map.count(xuid)) { + continue; // already added this xuid! + } + + bool in_use = false; + if (exclude_signed_in) { + // check if this xuid is signed in already + for (uint32_t i = 0; i < state->num_profiles(); i++) { + auto profile = state->user_profile(i); + if (!profile) { + continue; + } + if (profile->xuid() == xuid || profile->xuid_offline() == xuid) { + in_use = true; + break; + } + } + } + + if (!in_use) { + // Login with UserProfile so we can retrieve info about it + auto profile = std::make_unique(state); + if (!profile->Login(xuid)) { + continue; + } + + map[xuid] = std::tuple(profile_path, + profile->account_); + } + } + + return map; +} + std::string X_XAMACCOUNTINFO::GetGamertagString() const { return xe::to_string(std::wstring(gamertag)); } @@ -120,37 +246,27 @@ void UserProfile::EncryptAccountFile(const X_XAMACCOUNTINFO* input, UserProfile::UserProfile(KernelState* kernel_state) : kernel_state_(kernel_state), dash_gpd_(kDashboardID) { - base_path_ = - xe::to_absolute_path(kernel_state_->content_manager()->ResolvePackageRoot( - 0x10000, 0xFFFE07D1)); - - account_.xuid_online = cvars::profile_xuid; - wcscpy(account_.gamertag, L"XeniaUser"); - - // Try loading profile GPD files... - LoadProfile(); + base_path_ = base_path(kernel_state); // store path for later } -std::wstring UserProfile::MountProfile(const std::wstring& path) { +std::wstring UserProfile::ExtractProfile(const std::wstring& path) { auto package = path; auto package_dir = package + L".dir\\"; if (filesystem::PathExists(package_dir)) { return package_dir; } + // Get info about the path filesystem::FileInfo info; - // Get info for the original path too, in case we changed to the .dir above if (!filesystem::GetInfo(package, &info)) { - return L""; + // Path doesn't exist - return the .dir version to create later + return package_dir; } - auto file_system = kernel_state_->file_system(); - auto file_name = xe::to_string(info.name); - auto mount_path = "\\Device\\Profile_" + file_name; - - // If package still points to a file, open it as STFS and extract + // If path points to a file, open it as STFS and extract if (info.type == filesystem::FileInfo::Type::kFile) { XELOGI("MountProfile: extracting STFS profile %S", package.c_str()); + auto mount_path = "\\Device\\Profile_" + xe::to_string(info.name); // Register the container in the virtual filesystem. auto device = @@ -166,63 +282,141 @@ std::wstring UserProfile::MountProfile(const std::wstring& path) { return package_dir; } + // Must be an existing directory, just return the path for it return package + L"\\"; } -bool UserProfile::LoadProfile() { - auto profile_path = path(cvars::profile_xuid); - if (cvars::profile_xuid == -1) { - auto files = filesystem::ListFiles(path()); - // TODO: allow choosing by index maybe? - for (auto f : files) { - profile_path = f.path + f.name; - break; // use first found dir/package as profile +bool UserProfile::Login(uint64_t offline_xuid) { + xuid_offline_ = offline_xuid; + auto profile_path = path(xuid_offline_); + + if (xuid_offline_ == 1) { + // XUID = 1, login as the first non-signed-in profile + profile_path.clear(); + + auto profiles = Enumerate(kernel_state_, true); + if (profiles.size() > 0) { + auto& profile = profiles.begin(); + xuid_offline_ = profile->first; + profile_path = std::get<0>(profile->second); } } - profile_path_ = MountProfile(profile_path); + if (!xuid_offline_ || profile_path.empty()) { + XELOGW( + "UserProfile::Login: Couldn't find available profile to login to, " + "using temp. profile"); + + memset(&account_, 0, sizeof(X_XAMACCOUNTINFO)); + // Create some unique IDs for the profile + static int num_xuids = 0; + xuid_offline_ = 0xE0000000BEEFCAFE + num_xuids; + account_.xuid_online = 0x09000000BEEFCAFE + num_xuids; + swprintf_s(account_.gamertag, L"XeniaUser%d", num_xuids); + num_xuids++; + profile_path = path(xuid_offline_); + } + + // Try extracting profile to a folder, so we can modify any contents + profile_path_ = ExtractProfile(profile_path); if (profile_path_.empty()) { + XELOGW("UserProfile::Login: Failed to extract profile from %S!", + profile_path.c_str()); return false; } - auto mmap_ = - MappedMemory::Open(path() + L"Account", MappedMemory::Mode::kRead); - if (mmap_) { - XELOGI("Loading Account file from path %SAccount", path().c_str()); - - X_XAMACCOUNTINFO tmp_acct; - bool success = DecryptAccountFile(mmap_->data(), &tmp_acct); - if (!success) { - success = DecryptAccountFile(mmap_->data(), &tmp_acct, true); - } - - if (!success) { - XELOGW("Failed to decrypt Account file data"); + if (!filesystem::PathExists(profile_path_)) { + // Profile path doesn't exist - create new profile! + if (!filesystem::CreateFolder(profile_path_)) { + XELOGE("UserProfile::Login: Failed to create profile for '%S' at %S!", + account_.gamertag, path().c_str()); } else { - std::memcpy(&account_, &tmp_acct, sizeof(X_XAMACCOUNTINFO)); - XELOGI("Loaded Account \"%s\" successfully!", name().c_str()); + // Write out an account file + filesystem::CreateFile( + path() + L"Account"); // MappedMemory needs an existing file... + auto mmap_ = MappedMemory::Open(path() + L"Account", + MappedMemory::Mode::kReadWrite, 0, + sizeof(X_XAMACCOUNTINFO) + 0x18); + if (!mmap_) { + XELOGE("UserProfile::Login: Failed to create new Account at %S!", + path().c_str()); + } else { + XELOGI("Writing Account file for '%S' to path %SAccount", + account_.gamertag, path().c_str()); + EncryptAccountFile(&account_, mmap_->data(), false); + mmap_->Close(); + } + + // Dash GPD will be updated below, after default settings are added + } + } else { + // Profile exists, load Account and any GPDs + auto mmap_ = + MappedMemory::Open(path() + L"Account", MappedMemory::Mode::kRead); + if (mmap_) { + XELOGI("Loading Account file from path %SAccount", path().c_str()); + + X_XAMACCOUNTINFO tmp_acct; + bool success = DecryptAccountFile(mmap_->data(), &tmp_acct); + if (!success) { + success = DecryptAccountFile(mmap_->data(), &tmp_acct, true); + } + + if (!success) { + XELOGW("Failed to decrypt Account file data"); + } else { + std::memcpy(&account_, &tmp_acct, sizeof(X_XAMACCOUNTINFO)); + XELOGI("Loaded Account '%s' successfully!", name().c_str()); + } + + mmap_->Close(); } - mmap_->Close(); - } + XELOGD("Loading profile GPDs from path %S", path().c_str()); - XELOGI("Loading profile GPDs from path %S", path().c_str()); + mmap_ = + MappedMemory::Open(path() + L"FFFE07D1.gpd", MappedMemory::Mode::kRead); + if (mmap_) { + dash_gpd_.Read(mmap_->data(), mmap_->size()); + mmap_->Close(); + } else { + XELOGW("Failed to read dash GPD (FFFE07D1.gpd), using blank one"); - mmap_ = - MappedMemory::Open(path() + L"FFFE07D1.gpd", MappedMemory::Mode::kRead); - if (mmap_) { - dash_gpd_.Read(mmap_->data(), mmap_->size()); - mmap_->Close(); - } else { - XELOGW("Failed to read dash GPD (FFFE07D1.gpd), using blank one"); + // Create empty settings syncdata, helps tools identify this XDBF as a GPD + xdbf::Entry ent; + ent.info.section = static_cast(xdbf::GpdSection::kSetting); + ent.info.id = 0x200000000; + ent.data.resize(0x18); + memset(ent.data.data(), 0, 0x18); + dash_gpd_.UpdateEntry(ent); + } - // Create empty settings syncdata, helps tools identify this XDBF as a GPD - xdbf::Entry ent; - ent.info.section = static_cast(xdbf::GpdSection::kSetting); - ent.info.id = 0x200000000; - ent.data.resize(0x18); - memset(ent.data.data(), 0, 0x18); - dash_gpd_.UpdateEntry(ent); + // Load in any extra game GPDs + std::vector titles; + dash_gpd_.GetTitles(&titles); + + for (auto title : titles) { + wchar_t fname[256]; + swprintf(fname, 256, L"%X.gpd", title.title_id); + mmap_ = MappedMemory::Open(path() + fname, MappedMemory::Mode::kRead); + if (!mmap_) { + XELOGE("Failed to open GPD for title %X (%s)!", title.title_id, + xe::to_string(title.title_name).c_str()); + continue; + } + + xdbf::GpdFile title_gpd(title.title_id); + bool result = title_gpd.Read(mmap_->data(), mmap_->size()); + mmap_->Close(); + + if (!result) { + XELOGE("Failed to read GPD for title %X (%s)!", title.title_id, + xe::to_string(title.title_name).c_str()); + continue; + } + + title_gpds_[title.title_id] = title_gpd; + } } // Add some default settings games/apps usually ask for @@ -291,37 +485,14 @@ bool UserProfile::LoadProfile() { // Make sure the dash GPD is up-to-date UpdateGpd(kDashboardID, dash_gpd_); - // Load in any extra game GPDs - std::vector titles; - dash_gpd_.GetTitles(&titles); - - for (auto title : titles) { - wchar_t fname[256]; - swprintf(fname, 256, L"%X.gpd", title.title_id); - mmap_ = MappedMemory::Open(path() + fname, MappedMemory::Mode::kRead); - if (!mmap_) { - XELOGE("Failed to open GPD for title %X (%s)!", title.title_id, - xe::to_string(title.title_name).c_str()); - continue; - } - - xdbf::GpdFile title_gpd(title.title_id); - bool result = title_gpd.Read(mmap_->data(), mmap_->size()); - mmap_->Close(); - - if (!result) { - XELOGE("Failed to read GPD for title %X (%s)!", title.title_id, - xe::to_string(title.title_name).c_str()); - continue; - } - - title_gpds_[title.title_id] = title_gpd; - } - XELOGI("Loaded %d profile GPDs", title_gpds_.size()); return true; } +void UserProfile::Logout() { + *this = UserProfile(kernel_state_); // Reset ourselves +} + xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) { curr_title_id_ = 0; curr_gpd_ = nullptr; @@ -335,6 +506,8 @@ xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) { // TODO: let user choose locale? spa_data.GetAchievements(spa_data.GetDefaultLocale(), &spa_achievements); + // Check if title should be included in the dash GPD title list + // These checks should hopefully match the same checks X360 uses bool title_included = title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kFull || title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kDownload; diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 1568b544b..b204547e6 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -28,6 +28,7 @@ namespace xe { namespace kernel { namespace xam { +constexpr uint32_t kMaxNumUsers = 4; constexpr uint32_t kDashboardID = 0xFFFE07D1; // https://github.com/jogolden/testdev/blob/master/xkelib/xam/_xamext.h#L68 @@ -165,19 +166,33 @@ struct X_XAMACCOUNTINFO { class UserProfile { public: + static void CreateUsers(KernelState* kernel_state, + std::unique_ptr* profiles); + + // Returns map of OfflineXuid -> Path & AccountInfo pairs + static std::map> + Enumerate(KernelState* state, bool exclude_signed_in = false); + + static uint64_t XuidFromPath(const std::wstring& path); + 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); + static std::wstring base_path(KernelState* state); + UserProfile(KernelState* kernel_state); uint64_t xuid() const { return account_.xuid_online; } + uint64_t xuid_offline() const { return xuid_offline_; } std::string name() const { return account_.GetGamertagString(); } std::wstring path() const; std::wstring path(uint64_t xuid) const; - // uint32_t signin_state() const { return 1; } + uint32_t signin_state() const { return signin_state_; } + void signin_state(uint32_t state) { signin_state_ = state; } + bool signed_in() { return signin_state_ != 0 && xuid_offline_ != 0; } xdbf::GpdFile* SetTitleSpaData(const xdbf::SpaFile& spa_data); xdbf::GpdFile* GetTitleGpd(uint32_t title_id = -1); @@ -188,9 +203,16 @@ class UserProfile { bool UpdateTitleGpd(uint32_t title_id = -1); bool UpdateAllGpds(); + // Tries logging this user into a profile + // If XUID == 0, will use Xenia generated profile + // If XUID == 1, will try loading from any available profile + // If XUID is any other ID, will try loading from the profile it belongs to + bool Login(uint64_t offline_xuid = 0); + void Logout(); + private: - bool LoadProfile(); - std::wstring MountProfile(const std::wstring& path); + // Extracts profile package if needed - returns path to extracted folder + std::wstring ExtractProfile(const std::wstring& path); bool UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data); @@ -200,7 +222,10 @@ class UserProfile { std::wstring profile_path_; std::wstring base_path_; - X_XAMACCOUNTINFO account_; + + uint64_t xuid_offline_ = 0; + uint32_t signin_state_ = 0; + X_XAMACCOUNTINFO account_ = {0}; std::unordered_map title_gpds_; xdbf::GpdFile dash_gpd_; diff --git a/src/xenia/kernel/xam/xam_content.cc b/src/xenia/kernel/xam/xam_content.cc index 65496ffda..bbf857187 100644 --- a/src/xenia/kernel/xam/xam_content.cc +++ b/src/xenia/kernel/xam/xam_content.cc @@ -386,19 +386,23 @@ dword_result_t XamContentGetCreator(dword_t user_index, auto content_data = XCONTENT_DATA((uint8_t*)content_data_ptr); - if (content_data.content_type == 1) { - // User always creates saves. - *is_creator_ptr = 1; - if (creator_xuid_ptr) { - *creator_xuid_ptr = kernel_state()->user_profile()->xuid(); - } + auto user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + result = X_ERROR_NOT_LOGGED_ON; // TODO: find right error code } else { - *is_creator_ptr = 0; - if (creator_xuid_ptr) { - *creator_xuid_ptr = 0; + if (content_data.content_type == 1) { + // User always creates saves. + *is_creator_ptr = 1; + if (creator_xuid_ptr) { + *creator_xuid_ptr = user_profile->xuid(); + } + } else { + *is_creator_ptr = 0; + if (creator_xuid_ptr) { + *creator_xuid_ptr = 0; + } } } - if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result); return X_ERROR_IO_PENDING; diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 1bb838211..a30b08e28 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -19,8 +19,6 @@ #include "xenia/kernel/xthread.h" #include "xenia/xbox.h" -DEFINE_bool(signin_state, true, "User signed in", "Kernel"); - namespace xe { namespace kernel { namespace xam { @@ -31,7 +29,7 @@ struct X_PROFILEENUMRESULT { X_XAMACCOUNTINFO account; xe::be device_id; }; -// static_assert_size(X_PROFILEENUMRESULT, 0x188); +static_assert_size(X_PROFILEENUMRESULT, 0x188); dword_result_t XamProfileCreateEnumerator(dword_t device_id, lpdword_t handle_out) { @@ -42,17 +40,20 @@ dword_result_t XamProfileCreateEnumerator(dword_t device_id, e->Initialize(); - const auto& user_profile = kernel_state()->user_profile(); + auto profiles = UserProfile::Enumerate(kernel_state(), false); + for (auto profile : profiles) { + auto result = (X_PROFILEENUMRESULT*)e->AppendItem(); + memset(result, 0, sizeof(X_PROFILEENUMRESULT)); + result->xuid_offline = profile.first; + auto& acc = std::get<1>(profile.second); + memcpy(&result->account, &acc, sizeof(X_XAMACCOUNTINFO)); - X_PROFILEENUMRESULT* profile = (X_PROFILEENUMRESULT*)e->AppendItem(); - memset(profile, 0, sizeof(X_PROFILEENUMRESULT)); - profile->xuid_offline = user_profile->xuid(); - profile->device_id = 0xF00D0000; + // tag was swapped when account got loaded in, swap it back + xe::copy_and_swap(result->account.gamertag, + result->account.gamertag, 0x10); - auto tag = xe::to_wstring(user_profile->name()); - xe::copy_and_swap(profile->account.gamertag, tag.c_str(), - tag.length()); - profile->account.xuid_online = user_profile->xuid(); + result->device_id = 0xF00D0000; + } *handle_out = e->handle(); return X_ERROR_SUCCESS; @@ -104,10 +105,10 @@ DECLARE_XAM_EXPORT1(XamProfileEnumerate, kUserProfiles, kImplemented); X_HRESULT_result_t XamUserGetXUID(dword_t user_index, dword_t unk, lpqword_t xuid_ptr) { - if (user_index && user_index != 0xFF) { + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { return X_E_NO_SUCH_USER; } - const auto& user_profile = kernel_state()->user_profile(); if (xuid_ptr) { *xuid_ptr = user_profile->xuid(); } @@ -123,12 +124,8 @@ dword_result_t XamUserGetSigninState(dword_t user_index) { // This should keep games from asking us to sign in and also keep them // from initializing the network. - if (user_index == 0 || (user_index & 0xFF) == 0xFF) { - const auto& user_profile = kernel_state()->user_profile(); - return ((cvars::signin_state) ? 1 : 0); - } else { - return 0; - } + auto user_profile = kernel_state()->user_profile(user_index); + return user_profile ? user_profile->signin_state() : 0; } DECLARE_XAM_EXPORT2(XamUserGetSigninState, kUserProfiles, kImplemented, kHighFrequency); @@ -149,15 +146,15 @@ X_HRESULT_result_t XamUserGetSigninInfo(dword_t user_index, dword_t flags, return X_E_INVALIDARG; } - if (user_index && user_index != 0xFF) { + std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO)); + + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { return X_E_NO_SUCH_USER; } - std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO)); - - const auto& user_profile = kernel_state()->user_profile(); info->xuid = user_profile->xuid(); - info->signin_state = ((cvars::signin_state) ? 1 : 0); + info->signin_state = user_profile->signin_state(); std::strncpy(info->name, user_profile->name().data(), 15); return X_E_SUCCESS; } @@ -165,15 +162,15 @@ DECLARE_XAM_EXPORT1(XamUserGetSigninInfo, kUserProfiles, kImplemented); dword_result_t XamUserGetName(dword_t user_index, lpstring_t buffer, dword_t buffer_len) { - if (user_index && user_index != 0xFF) { - return X_ERROR_NO_SUCH_USER; - } - if (!buffer_len) { return X_ERROR_SUCCESS; } - const auto& user_profile = kernel_state()->user_profile(); + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return X_ERROR_NO_SUCH_USER; + } + const auto& user_name = user_profile->name(); // Real XAM will only copy a maximum of 15 characters out. @@ -187,15 +184,15 @@ DECLARE_XAM_EXPORT1(XamUserGetName, kUserProfiles, kImplemented); dword_result_t XamUserGetGamerTag(dword_t user_index, lpwstring_t buffer, dword_t buffer_len) { - if (user_index && user_index != 0xFF) { - return X_ERROR_NO_SUCH_USER; - } - if (!buffer_len) { return X_ERROR_SUCCESS; } - const auto& user_profile = kernel_state()->user_profile(); + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return X_ERROR_NO_SUCH_USER; + } + const auto& user_name = xe::to_wstring(user_profile->name()); size_t copy_length = std::min({size_t(buffer_len * 2), user_name.size(), @@ -245,8 +242,8 @@ dword_result_t XamUserReadProfileSettings( // Title ID = 0 means us. // 0xfffe07d1 = profile? - if (actual_user_index) { - // Only support user 0. + const auto& user_profile = kernel_state()->user_profile(actual_user_index); + if (!user_profile) { if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, X_ERROR_NOT_FOUND); @@ -255,8 +252,6 @@ dword_result_t XamUserReadProfileSettings( return X_ERROR_NOT_FOUND; } - const auto& user_profile = kernel_state()->user_profile(); - // First call asks for size (fill buffer_size_ptr). // Second call asks for buffer contents with that size. @@ -383,13 +378,9 @@ dword_result_t XamUserWriteProfileSettings( return X_ERROR_INVALID_PARAMETER; } - uint32_t actual_user_index = user_index; - if (actual_user_index == 255) { - actual_user_index = 0; - } - - if (actual_user_index) { - // Only support user 0. + // Update and save settings. + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, X_ERROR_NOT_FOUND); @@ -398,9 +389,6 @@ dword_result_t XamUserWriteProfileSettings( return X_ERROR_NOT_FOUND; } - // Update and save settings. - const auto& user_profile = kernel_state()->user_profile(); - auto gpd = user_profile->GetDashboardGpd(); if (title_id != 0xFFFE07D1) { gpd = user_profile->GetTitleGpd(title_id); @@ -471,13 +459,8 @@ DECLARE_XAM_EXPORT1(XamUserWriteProfileSettings, kUserProfiles, kImplemented); dword_result_t XamUserCheckPrivilege(dword_t user_index, dword_t mask, lpdword_t out_value) { - uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF) { - // Always pin user to 0. - actual_user_index = 0; - } - - if (actual_user_index) { + auto* user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { return X_ERROR_NO_SUCH_USER; } @@ -489,13 +472,8 @@ DECLARE_XAM_EXPORT1(XamUserCheckPrivilege, kUserProfiles, kStub); dword_result_t XamUserContentRestrictionGetFlags(dword_t user_index, lpdword_t out_flags) { - uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF) { - // Always pin user to 0. - actual_user_index = 0; - } - - if (actual_user_index) { + auto* user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { return X_ERROR_NO_SUCH_USER; } @@ -509,13 +487,8 @@ dword_result_t XamUserContentRestrictionGetRating(dword_t user_index, dword_t unk1, lpdword_t out_unk2, lpdword_t out_unk3) { - uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF) { - // Always pin user to 0. - actual_user_index = 0; - } - - if (actual_user_index) { + auto* user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { return X_ERROR_NO_SUCH_USER; } @@ -547,27 +520,20 @@ DECLARE_XAM_EXPORT1(XamUserContentRestrictionCheckAccess, kUserProfiles, kStub); dword_result_t XamUserAreUsersFriends(dword_t user_index, dword_t unk1, dword_t unk2, lpdword_t out_value, dword_t overlapped_ptr) { - uint32_t actual_user_index = user_index; - if ((actual_user_index & 0xFF) == 0xFF) { - // Always pin user to 0. - actual_user_index = 0; - } - *out_value = 0; - if (user_index) { - // Only support user 0. - return X_ERROR_NOT_LOGGED_ON; - } - X_RESULT result = X_ERROR_SUCCESS; - const auto& user_profile = kernel_state()->user_profile(); - if (((cvars::signin_state) ? 1 : 0) == 0) { + const auto& user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { result = X_ERROR_NOT_LOGGED_ON; } else { - // No friends! - *out_value = 0; + if (!user_profile->signin_state()) { + result = X_ERROR_NOT_LOGGED_ON; + } else { + // No friends! + *out_value = 0; + } } if (overlapped_ptr) { @@ -606,6 +572,11 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id, dword_t offset, dword_t count, lpdword_t buffer_size_ptr, lpdword_t handle_ptr) { + auto user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return X_ERROR_INVALID_PARAMETER; // TODO: proper error code! + } + if (buffer_size_ptr) { *buffer_size_ptr = sizeof(X_XACHIEVEMENT_DETAILS) * count; } @@ -617,7 +588,7 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id, *handle_ptr = e->handle(); // Copy achievements into the enumerator if game GPD is loaded - auto* game_gpd = kernel_state()->user_profile()->GetTitleGpd(title_id); + auto* game_gpd = user_profile->GetTitleGpd(title_id); if (!game_gpd) { XELOGE( "XamUserCreateAchievementEnumerator failed to find GPD for title %X!", @@ -718,12 +689,17 @@ dword_result_t XamUserCreateTitlesPlayedEnumerator( // + 128 bytes for the 64-char titlename const uint32_t kEntrySize = sizeof(xdbf::X_XDBF_GPD_TITLEPLAYED) + 128; + auto user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return X_ERROR_INVALID_PARAMETER; // TODO: proper error code! + } + if (buffer_size_ptr) { *buffer_size_ptr = kEntrySize * games_count; } std::vector titles; - kernel_state()->user_profile()->GetDashboardGpd()->GetTitles(&titles); + user_profile->GetDashboardGpd()->GetTitles(&titles); // Sort titles by date played std::sort(titles.begin(), titles.end(), @@ -760,12 +736,12 @@ DECLARE_XAM_EXPORT1(XamUserCreateTitlesPlayedEnumerator, kUserProfiles, kImplemented); dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, - dword_t offset, lpdword_t output_ptr, + dword_t user_index, lpdword_t output_ptr, lpdword_t buffer_size_ptr, dword_t overlapped_ptr) { // Wrap function in a lambda func so we can use return to exit out when // needed, but still always be able to set the xoverlapped value // this way we don't need a bunch of if/else nesting to accomplish the same - auto main_fn = [tile_type, game_id, item_id, offset, output_ptr, + auto main_fn = [tile_type, game_id, item_id, user_index, output_ptr, buffer_size_ptr]() { uint64_t image_id = item_id; @@ -773,6 +749,11 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, size_t data_len = 0; std::unique_ptr mmap; + auto user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return X_ERROR_INVALID_PARAMETER; // TODO: proper error code! + } + if (!output_ptr || !buffer_size_ptr) { return X_ERROR_FILE_NOT_FOUND; } @@ -781,7 +762,7 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, if (kTileFileNames.count(type)) { // image_id = XUID of profile to retrieve from - auto file_path = kernel_state()->user_profile()->path(); + auto file_path = user_profile->path(); file_path += kTileFileNames.at(type); mmap = MappedMemory::Open(file_path, MappedMemory::Mode::kRead); @@ -791,7 +772,7 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, data = mmap->data(); data_len = mmap->size(); } else { - auto gpd = kernel_state()->user_profile()->GetTitleGpd(game_id.value()); + auto gpd = user_profile->GetTitleGpd(game_id.value()); if (!gpd) { return X_ERROR_FILE_NOT_FOUND; @@ -851,16 +832,27 @@ DECLARE_XAM_EXPORT1(XamReadTileEx, kUserProfiles, kSketchy); dword_result_t XamUserIsOnlineEnabled(dword_t user_index) { // 0 - Offline // 1 - Online - return 1; + + auto* user_profile = kernel_state()->user_profile(user_index); + if (!user_profile) { + return 0; + } + + return user_profile->signin_state() == 2; } DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub); dword_result_t XamUserGetIndexFromXUID(qword_t xuid, dword_t r4, lpdword_t user_index) { - // TODO: support more than 1 user_index! - if (xuid == kernel_state()->user_profile()->xuid()) { - *user_index = 0; - return X_E_SUCCESS; + for (uint32_t i = 0; i < kernel_state()->num_profiles(); i++) { + auto profile = kernel_state()->user_profile(i); + if (!profile) { + continue; + } + if (profile->xuid() == xuid) { + *user_index = i; + return X_E_SUCCESS; + } } return X_E_NO_SUCH_USER;