/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2025 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/string_util.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/user_profile.h" #include "xenia/kernel/xam/user_settings.h" #include "xenia/kernel/xam/xam_private.h" #include "xenia/kernel/xenumerator.h" #include "xenia/kernel/xthread.h" #include "xenia/xbox.h" #include "third_party/stb/stb_image.h" DECLARE_int32(user_language); namespace xe { namespace kernel { namespace xam { X_HRESULT_result_t XamUserGetXUID_entry(dword_t user_index, dword_t type_mask, lpqword_t xuid_ptr) { assert_true(type_mask == 1 || type_mask == 2 || type_mask == 3 || type_mask == 4 || type_mask == 7); 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; 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; } DECLARE_XAM_EXPORT1(XamUserGetXUID, kUserProfiles, kImplemented); dword_result_t XamUserGetIndexFromXUID_entry(qword_t xuid, dword_t flags, pointer_t index) { if (!index) { return X_E_INVALIDARG; } const uint8_t user_index = kernel_state() ->xam_state() ->profile_manager() ->GetUserIndexAssignedToProfile(xuid); if (user_index == XUserIndexAny) { return X_E_NO_SUCH_USER; } *index = user_index; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetIndexFromXUID, kUserProfiles, kImplemented); 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 >= 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; } DECLARE_XAM_EXPORT2(XamUserGetSigninState, kUserProfiles, kImplemented, kHighFrequency); typedef struct { xe::be xuid; xe::be flags; xe::be signin_state; xe::be guest_num; xe::be sponsor_user_index; char name[16]; } X_USER_SIGNIN_INFO; static_assert_size(X_USER_SIGNIN_INFO, 40); X_HRESULT_result_t XamUserGetSigninInfo_entry( dword_t user_index, dword_t flags, pointer_t info) { if (!info) { return X_E_INVALIDARG; } std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO)); if (user_index >= XUserMaxUserCount) { return X_E_NO_SUCH_USER; } if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); info->xuid = user_profile->xuid(); info->signin_state = user_profile->signin_state(); xe::string_util::copy_truncating(info->name, user_profile->name(), xe::countof(info->name)); } else { return X_E_NO_SUCH_USER; } return X_E_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetSigninInfo, kUserProfiles, kImplemented); dword_result_t XamUserGetName_entry(dword_t user_index, dword_t buffer, dword_t buffer_len) { if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { // Based on XAM only first byte is cleared in case of lack of user. kernel_memory()->Zero(buffer, 1); return X_ERROR_NO_SUCH_USER; } const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); // Because name is always limited to 15 characters we can assume length will // never exceed that limit. const auto& user_name = user_profile->name(); // buffer_len includes null-terminator. user_name does not. const uint32_t bytes_to_copy = std::min( buffer_len.value(), static_cast(user_name.length()) + 1); char* str_buffer = kernel_memory()->TranslateVirtual(buffer); xe::string_util::copy_truncating(str_buffer, user_name, bytes_to_copy); return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetName, kUserProfiles, kImplemented); dword_result_t XamUserGetGamerTag_entry(dword_t user_index, dword_t buffer, dword_t buffer_len) { if (!buffer || buffer_len < 16) { return X_E_INVALIDARG; } if (user_index >= XUserMaxUserCount) { return X_E_INVALIDARG; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); auto user_name = xe::to_utf16(user_profile->name()); char16_t* str_buffer = kernel_memory()->TranslateVirtual(buffer); xe::string_util::copy_and_swap_truncating( str_buffer, user_name, std::min(buffer_len.value(), uint32_t(16))); return X_E_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetGamerTag, kUserProfiles, kImplemented); typedef struct { xe::be setting_count; xe::be settings_ptr; } X_USER_READ_PROFILE_SETTINGS; static_assert_size(X_USER_READ_PROFILE_SETTINGS, 8); // https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/Generic/xboxtools.cpp uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, uint32_t xuid_count, be* xuids, uint32_t setting_count, be* setting_ids, uint32_t unk, be* buffer_size_ptr, uint8_t* buffer, lpvoid_t overlapped_ptr) { assert_zero(unk); // probably flags // must have at least 1 to 32 settings if (setting_count < 1 || setting_count > 32) { return X_ERROR_INVALID_PARAMETER; } // buffer size pointer must be valid if (!buffer_size_ptr) { return X_ERROR_INVALID_PARAMETER; } // if buffer size is non-zero, buffer pointer must be valid auto buffer_size = static_cast(*buffer_size_ptr); if (buffer_size && !buffer) { return X_ERROR_INVALID_PARAMETER; } uint32_t needed_header_size = 0; uint32_t needed_data_size = 0; for (uint32_t i = 0; i < setting_count; ++i) { needed_header_size += sizeof(X_USER_PROFILE_SETTING); AttributeKey setting_key; setting_key.value = static_cast(setting_ids[i]); switch (static_cast(setting_key.type)) { case X_USER_DATA_TYPE::WSTRING: case X_USER_DATA_TYPE::BINARY: needed_data_size += setting_key.size; break; default: break; } } if (xuids) { needed_header_size *= xuid_count; needed_data_size *= xuid_count; } needed_header_size += sizeof(X_USER_READ_PROFILE_SETTINGS); uint32_t needed_size = needed_header_size + needed_data_size; if (!buffer || buffer_size < needed_size) { if (!buffer_size) { *buffer_size_ptr = needed_size; } return X_ERROR_INSUFFICIENT_BUFFER; } auto run = [=](uint32_t& extended_error, uint32_t& length) { auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user_profile && !xuids) { return X_ERROR_NO_SUCH_USER; } if (xuids) { uint64_t user_xuid = static_cast(xuids[0]); if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) { return X_ERROR_NO_SUCH_USER; } user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid); } if (!user_profile) { return X_ERROR_NO_SUCH_USER; } auto out_header = reinterpret_cast(buffer); auto out_setting = reinterpret_cast(&out_header[1]); out_header->setting_count = static_cast(setting_count); out_header->settings_ptr = kernel_state()->memory()->HostToGuestVirtual(out_setting); uint32_t additional_data_buffer_ptr = out_header->settings_ptr + (setting_count * sizeof(X_USER_PROFILE_SETTING)); std::fill_n(out_setting, setting_count, X_USER_PROFILE_SETTING{}); for (uint32_t n = 0; n < setting_count; ++n) { const uint32_t setting_id = setting_ids[n]; if (!UserSetting::is_setting_valid(setting_id)) { if (setting_id != 0) { XELOGE( "xeXamUserReadProfileSettingsEx requested unimplemented setting " "{:08X}", setting_id); } --out_header->setting_count; continue; } const bool is_valid = kernel_state()->xam_state()->user_tracker()->GetUserSetting( user_profile->xuid(), title_id ? title_id : kernel_state()->title_id(), setting_id, out_setting, additional_data_buffer_ptr); if (is_valid) { if (xuids) { out_setting->xuid = user_profile->xuid(); } else { out_setting->xuid = -1; out_setting->user_index = user_index; } } ++out_setting; } extended_error = X_HRESULT_FROM_WIN32(X_STATUS_SUCCESS); length = 0; return X_STATUS_SUCCESS; }; if (!overlapped_ptr) { uint32_t extended_error, length; return run(extended_error, length); } kernel_state()->CompleteOverlappedDeferredEx(run, overlapped_ptr); return X_ERROR_IO_PENDING; } dword_result_t XamUserReadProfileSettings_entry( dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids, dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr, lpvoid_t buffer_ptr, lpvoid_t overlapped) { return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids, setting_count, setting_ids, 0, buffer_size_ptr, buffer_ptr, overlapped); } DECLARE_XAM_EXPORT1(XamUserReadProfileSettings, kUserProfiles, kImplemented); dword_result_t XamUserReadProfileSettingsEx_entry( dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids, dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr, dword_t unk_2, lpvoid_t buffer_ptr, lpvoid_t overlapped) { return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids, setting_count, setting_ids, unk_2, buffer_size_ptr, buffer_ptr, overlapped); } DECLARE_XAM_EXPORT1(XamUserReadProfileSettingsEx, kUserProfiles, kImplemented); dword_result_t XamUserWriteProfileSettings_entry( dword_t title_id, dword_t user_index, dword_t setting_count, pointer_t settings, lpvoid_t overlapped) { if (!setting_count || !settings) { return X_ERROR_INVALID_PARAMETER; } auto run = [=](uint32_t& extended_error, uint32_t& length) { // 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_profile) { extended_error = X_HRESULT_FROM_WIN32(X_ERROR_NO_SUCH_USER); length = 0; return X_ERROR_NO_SUCH_USER; } for (uint32_t n = 0; n < setting_count; ++n) { const UserSetting setting = UserSetting(&settings[n]); if (!setting.is_valid_type()) { continue; } kernel_state()->xam_state()->user_tracker()->UpsertSetting( user_profile->xuid(), title_id, &setting); } extended_error = X_HRESULT_FROM_WIN32(X_STATUS_SUCCESS); length = 0; return X_STATUS_SUCCESS; }; if (!overlapped) { uint32_t extended_error, length; return run(extended_error, length); } kernel_state()->CompleteOverlappedDeferredEx(run, overlapped); return X_ERROR_IO_PENDING; } 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 != XUserIndexAny) { if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } } // If we deny everything, games should hopefully not try to do stuff. *out_value = 0; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserCheckPrivilege, kUserProfiles, kStub); dword_result_t XamUserContentRestrictionGetFlags_entry(dword_t user_index, lpdword_t out_flags) { if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } // No restrictions? *out_flags = 0; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserContentRestrictionGetFlags, kUserProfiles, kStub); dword_result_t XamUserContentRestrictionGetRating_entry(dword_t user_index, dword_t unk1, lpdword_t out_unk2, lpdword_t out_unk3) { if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } // Some games have special case paths for 3F that differ from the failure // path, so my guess is that's 'don't care'. *out_unk2 = 0x3F; *out_unk3 = 0; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserContentRestrictionGetRating, kUserProfiles, kStub); dword_result_t XamUserContentRestrictionCheckAccess_entry( dword_t user_index, dword_t unk1, dword_t unk2, dword_t unk3, dword_t unk4, lpdword_t out_unk5, dword_t overlapped_ptr) { *out_unk5 = 1; if (overlapped_ptr) { // TODO(benvanik): does this need the access arg on it? kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, X_ERROR_SUCCESS); } return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserContentRestrictionCheckAccess, kUserProfiles, kStub); dword_result_t XamUserIsOnlineEnabled_entry(dword_t user_index) { if (user_index >= XUserMaxUserCount) { return 0; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return 0; } return kernel_state() ->xam_state() ->GetUserProfile(user_index) ->IsLiveEnabled(); } DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kImplemented); dword_result_t XamUserGetMembershipTier_entry(dword_t user_index) { if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } return kernel_state() ->xam_state() ->GetUserProfile(user_index) ->GetSubscriptionTier(); } DECLARE_XAM_EXPORT1(XamUserGetMembershipTier, kUserProfiles, kImplemented); dword_result_t XamUserGetMembershipTierFromXUID_entry(qword_t xuid) { const auto profile = kernel_state()->xam_state()->GetUserProfile(xuid); if (!profile) { return 0; } return profile->GetSubscriptionTier(); } DECLARE_XAM_EXPORT1(XamUserGetMembershipTierFromXUID, kUserProfiles, kImplemented); dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1, dword_t unk2, lpdword_t out_value, dword_t overlapped_ptr) { uint32_t are_friends = 0; X_RESULT result; if (user_index >= XUserMaxUserCount) { result = X_ERROR_INVALID_PARAMETER; } else { if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); if (user_profile->signin_state() == 0) { result = X_ERROR_NOT_LOGGED_ON; } else { // No friends! are_friends = 0; result = X_ERROR_SUCCESS; } } else { // Only support user 0. result = X_ERROR_NO_SUCH_USER; // if user is local -> X_ERROR_NOT_LOGGED_ON } } if (out_value) { assert_true(!overlapped_ptr); *out_value = result == X_ERROR_SUCCESS ? are_friends : 0; return result; } else if (overlapped_ptr) { assert_true(!out_value); kernel_state()->CompleteOverlappedImmediateEx( overlapped_ptr, result == X_ERROR_SUCCESS ? X_ERROR_SUCCESS : X_ERROR_FUNCTION_FAILED, X_HRESULT_FROM_WIN32(result), result == X_ERROR_SUCCESS ? are_friends : 0); return X_ERROR_IO_PENDING; } else { assert_always(); return X_ERROR_INVALID_PARAMETER; } } DECLARE_XAM_EXPORT1(XamUserAreUsersFriends, kUserProfiles, kStub); dword_result_t XamUserCreateAchievementEnumerator_entry( dword_t title_id, dword_t user_index, qword_t xuid, dword_t flags, dword_t offset, dword_t count, lpdword_t buffer_size_ptr, lpdword_t handle_ptr) { if (!count || !buffer_size_ptr || !handle_ptr) { return X_ERROR_INVALID_PARAMETER; } if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } size_t entry_size = sizeof(X_ACHIEVEMENT_DETAILS); if (flags & 7) { entry_size += X_ACHIEVEMENT_DETAILS::kStringBufferSize; } if (buffer_size_ptr) { *buffer_size_ptr = static_cast(entry_size) * count; } auto e = object_ref( new XAchievementEnumerator(kernel_state(), count, flags)); auto result = e->Initialize(user_index, 0xFB, 0xB000A, 0xB000B, 0); if (XFAILED(result)) { return result; } const auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_INVALID_PARAMETER; } uint64_t requester_xuid = user->xuid(); if (xuid) { requester_xuid = xuid; } const uint32_t title_id_ = title_id ? static_cast(title_id) : kernel_state()->title_id(); const auto user_title_achievements = kernel_state()->achievement_manager()->GetTitleAchievements( requester_xuid, title_id_); if (!user_title_achievements.empty()) { for (const auto& entry : user_title_achievements) { auto unlock_time = X_FILETIME(); if (entry.IsUnlocked() && entry.unlock_time.is_valid()) { unlock_time = entry.unlock_time; } auto item = AchievementDetails( entry.achievement_id, entry.achievement_name.c_str(), entry.unlocked_description.c_str(), entry.locked_description.c_str(), entry.image_id, entry.gamerscore, unlock_time, entry.flags); e->AppendItem(item); } } *handle_ptr = e->handle(); return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserCreateAchievementEnumerator, kUserProfiles, kSketchy); dword_result_t XamUserCreateTitlesPlayedEnumerator_entry( dword_t title_id, dword_t user_index, qword_t xuid, dword_t starting_index, dword_t game_count, lpdword_t buffer_size_ptr, lpdword_t handle_ptr) { if (user_index >= XUserMaxUserCount && game_count != 0 && !buffer_size_ptr && !handle_ptr) { return X_ERROR_INVALID_PARAMETER; } const uint32_t kEntrySize = sizeof(XTitleEnumerator::XTITLE_PLAYED); if (buffer_size_ptr) { *buffer_size_ptr = kEntrySize * game_count; } const auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_INVALID_PARAMETER; } auto e = object_ref( new XTitleEnumerator(kernel_state(), game_count)); auto result = e->Initialize(user_index, 0xFB, 0xB0050, 0xB000B, 0x20, game_count, 0); if (XFAILED(result)) { return result; } const auto user_titles = kernel_state()->xam_state()->user_tracker()->GetPlayedTitles( user->xuid()); if (!user_titles.empty()) { for (const auto& title : user_titles) { if (title.id == kDashboardID) { continue; } if (!title.achievements_count || !title.gamerscore_amount) { continue; } e->AppendItem(title); } } *handle_ptr = e->handle(); return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserCreateTitlesPlayedEnumerator, kUserProfiles, kStub); dword_result_t XamReadTile_entry(dword_t tile_type, dword_t title_id, qword_t item_id, dword_t user_index, lpdword_t output_ptr, lpdword_t buffer_size_ptr, lpvoid_t overlapped_ptr) { auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { user = kernel_state()->xam_state()->GetUserProfile(item_id); if (!user) { return X_ERROR_INVALID_PARAMETER; } } if (!buffer_size_ptr) { return X_ERROR_INVALID_PARAMETER; } auto run = [=](uint32_t& extended_error, uint32_t& length) { std::span tile = kernel_state()->xam_state()->user_tracker()->GetIcon( user->xuid(), title_id, static_cast(tile_type.value()), item_id); auto result = X_ERROR_SUCCESS; if (tile.empty()) { result = X_ERROR_FILE_NOT_FOUND; } *buffer_size_ptr = static_cast(tile.size()); if (output_ptr) { memcpy(output_ptr, tile.data(), tile.size()); } else { result = X_ERROR_INSUFFICIENT_BUFFER; } extended_error = X_HRESULT_FROM_WIN32(result); length = 0; return result; }; if (!overlapped_ptr) { uint32_t extended_error, length; return run(extended_error, length); } kernel_state()->CompleteOverlappedDeferredEx(run, overlapped_ptr); return X_ERROR_IO_PENDING; } DECLARE_XAM_EXPORT1(XamReadTile, kUserProfiles, kSketchy); dword_result_t XamReadTileEx_entry(dword_t tile_type, dword_t game_id, qword_t item_id, dword_t offset, dword_t unk1, dword_t unk2, lpdword_t output_ptr, lpdword_t buffer_size_ptr) { return XamReadTile_entry(tile_type, game_id, item_id, offset, output_ptr, buffer_size_ptr, 0); } DECLARE_XAM_EXPORT1(XamReadTileEx, kUserProfiles, kSketchy); dword_result_t XamParseGamerTileKey_entry(pointer_t key_ptr, lpdword_t title_id_ptr, lpdword_t big_tile_id_ptr, lpdword_t small_tile_id_ptr) { if (!key_ptr) { return X_ERROR_INVALID_PARAMETER; } if (key_ptr->type != X_USER_DATA_TYPE::WSTRING) { return X_ERROR_INVALID_PARAMETER; } if (key_ptr->data.unicode.size > 0x64) { return X_ERROR_INVALID_PARAMETER; } if (!key_ptr->data.unicode.ptr) { return X_ERROR_INVALID_PARAMETER; } std::string tile_key = xe::to_utf8(string_util::read_u16string_and_swap( kernel_memory()->TranslateVirtual( key_ptr->data.unicode.ptr))); // Default key size is 24 bytes, but we need to include null terminator if (tile_key.empty() || tile_key.size() != 25) { return X_ERROR_INVALID_PARAMETER; } const bool is_valid_hex_string = std::all_of(tile_key.begin(), --tile_key.end(), [](unsigned char c) { return std::isxdigit(c); }); if (!is_valid_hex_string) { return X_ERROR_INVALID_PARAMETER; } // Simple parser for key. Key (lower case) contains: title_id (8 chars), // big_tile_id (8 chars), small_tile_id (8 chars) std::string title_id = tile_key.substr(0, 8); std::string big_tile_id = tile_key.substr(8, 8); std::string small_tile_id = tile_key.substr(16, 8); if (title_id_ptr) { *title_id_ptr = string_util::from_string(title_id, true); } if (big_tile_id_ptr) { *big_tile_id_ptr = string_util::from_string(big_tile_id, true); } if (small_tile_id_ptr) { *small_tile_id_ptr = string_util::from_string(small_tile_id, true); } return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamParseGamerTileKey, kUserProfiles, kImplemented); dword_result_t XamReadTileToTexture_entry(dword_t tile_type, dword_t title_id, qword_t tile_id, dword_t user_index, lpvoid_t buffer_ptr, dword_t stride, dword_t tile_height, dword_t overlapped_ptr) { if (!buffer_ptr) { return X_ERROR_INVALID_PARAMETER; } size_t buffer_size = size_t(stride) * size_t(tile_height); auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_INVALID_PARAMETER; } std::span tile = kernel_state()->xam_state()->user_tracker()->GetIcon( user->xuid(), title_id, static_cast(tile_type.value()), tile_id); if (tile.empty()) { return X_ERROR_SUCCESS; } int width, height, channels; unsigned char* imageData = stbi_load_from_memory(tile.data(), static_cast(tile.size()), &width, &height, &channels, STBI_rgb_alpha); size_t icon_dimmension_size = width * height; std::fill_n(reinterpret_cast(buffer_ptr.host_address()), icon_dimmension_size * sizeof(uint32_t), 0); for (int i = 0; i < icon_dimmension_size; i++) { unsigned char* pixel = &imageData[i * sizeof(uint32_t)]; // RGBA to ARGB. TODO: Find faster method! // RGBA->AGBR std::swap(pixel[0], pixel[3]); // AGBR->ARBG std::swap(pixel[1], pixel[3]); // ARBG->ARGB std::swap(pixel[2], pixel[3]); } memcpy(buffer_ptr, imageData, std::min(buffer_size, static_cast(icon_dimmension_size * sizeof(uint32_t)))); stbi_image_free(imageData); if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, X_ERROR_SUCCESS); return X_ERROR_IO_PENDING; } return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamReadTileToTexture, kUserProfiles, kStub); dword_result_t XamWriteGamerTile_entry(dword_t user_index, dword_t title_id, dword_t small_tile_id, dword_t big_tile_id, dword_t arg5, dword_t overlapped_ptr) { if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, X_ERROR_SUCCESS); return X_ERROR_IO_PENDING; } return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamWriteGamerTile, kUserProfiles, kStub); dword_result_t XamSessionCreateHandle_entry(lpdword_t handle_ptr) { *handle_ptr = 0xCAFEDEAD; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamSessionCreateHandle, kUserProfiles, kStub); dword_result_t XamSessionRefObjByHandle_entry(dword_t handle, lpdword_t obj_ptr) { assert_true(handle == 0xCAFEDEAD); // TODO(PermaNull): Implement this properly, // For the time being returning 0xDEADF00D will prevent crashing. *obj_ptr = 0xDEADF00D; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamSessionRefObjByHandle, kUserProfiles, kStub); dword_result_t XamUserIsUnsafeProgrammingAllowed_entry(dword_t user_index, dword_t unk, lpdword_t result_ptr) { if (!result_ptr) { return X_ERROR_INVALID_PARAMETER; } if (user_index != XUserIndexAny && user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } // uint32_t result = XamUserCheckPrivilege_entry(user_index, 0xD4u, // result_ptr); *result_ptr = 1; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserIsUnsafeProgrammingAllowed, kUserProfiles, kStub); dword_result_t XamUserGetSubscriptionType_entry(dword_t user_index, lpdword_t subscription_ptr, lpdword_t r5, dword_t overlapped_ptr) { if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!subscription_ptr || !r5) { return X_E_INVALIDARG; } auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_INVALID_PARAMETER; } *subscription_ptr = user->GetSubscriptionTier(); *r5 = 0x0; return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetSubscriptionType, kUserProfiles, kStub); dword_result_t XamUserGetUserFlags_entry(dword_t user_index) { if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return 0; } const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); return user_profile->GetCachedFlags(); } DECLARE_XAM_EXPORT1(XamUserGetUserFlags, kUserProfiles, kImplemented); dword_result_t XamUserGetUserFlagsFromXUID_entry(qword_t xuid) { const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(xuid); if (!user_profile) { return 0; } return user_profile->GetCachedFlags(); } DECLARE_XAM_EXPORT1(XamUserGetUserFlagsFromXUID, kUserProfiles, kImplemented); dword_result_t XamUserGetOnlineLanguageFromXUID_entry(qword_t xuid) { /* Notes: - Calls XamUserGetUserFlagsFromXUID and returns (ulonglong)(cached_flag << 0x20) >> 0x39 & 0x1f; - XamUserGetMembershipTierFromXUID and XamUserGetOnlineCountryFromXUID also call it - Removed in metro */ return cvars::user_language; } DECLARE_XAM_EXPORT1(XamUserGetOnlineLanguageFromXUID, kUserProfiles, kStub); constexpr uint8_t kStatsMaxAmount = 64; struct X_STATS_DETAILS { xe::be id; xe::be stats_amount; xe::be stats[kStatsMaxAmount]; }; static_assert_size(X_STATS_DETAILS, 8 + kStatsMaxAmount * 2); dword_result_t XamUserCreateStatsEnumerator_entry( dword_t title_id, dword_t user_index, dword_t count, dword_t flags, dword_t size, pointer_t stats_ptr, lpdword_t buffer_size_ptr, lpdword_t handle_ptr) { if (!count || !buffer_size_ptr || !handle_ptr || !stats_ptr) { return X_ERROR_INVALID_PARAMETER; } if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!flags || flags > 0x64) { return X_ERROR_INVALID_PARAMETER; } if (!size) { return X_ERROR_INVALID_PARAMETER; } if (buffer_size_ptr) { *buffer_size_ptr = 0; // sizeof(X_STATS_DETAILS) * stats_ptr->stats_amount; } auto e = object_ref( new XUserStatsEnumerator(kernel_state(), 0)); const X_STATUS result = e->Initialize(user_index, 0xFB, 0xB0023, 0xB0024, 0); if (XFAILED(result)) { return result; } *handle_ptr = e->handle(); return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserCreateStatsEnumerator, kUserProfiles, kSketchy); dword_result_t XamUserGetUserTenure_entry(dword_t user_index, lpdword_t tenure_level_ptr, lpdword_t milestone_ptr, lpqword_t milestone_date_ptr, dword_t overlap_ptr) { if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_E_INVALIDARG; } const auto& user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); if (const auto setting = kernel_state()->xam_state()->user_tracker()->GetSetting( user_profile, kDashboardID, static_cast(UserSettingId::XPROFILE_TENURE_LEVEL))) { *tenure_level_ptr = std::get(setting->get_host_data()); } if (const auto setting = kernel_state()->xam_state()->user_tracker()->GetSetting( user_profile, kDashboardID, static_cast( UserSettingId::XPROFILE_TENURE_MILESTONE))) { *milestone_ptr = std::get(setting->get_host_data()); } if (const auto setting = kernel_state()->xam_state()->user_tracker()->GetSetting( user_profile, kDashboardID, static_cast( UserSettingId::XPROFILE_TENURE_NEXT_MILESTONE_DATE))) { *milestone_date_ptr = std::get(setting->get_host_data()); } return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserGetUserTenure, kUserProfiles, kImplemented); } // namespace xam } // namespace kernel } // namespace xe DECLARE_XAM_EMPTY_REGISTER_EXPORTS(User);