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;