diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index ac5701c0f..d5bab97cf 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1794,10 +1794,11 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey( } if (!notificationTitle.empty()) { - app_context_.CallInUIThread([=]() { - new xe::ui::HostNotificationWindow(imgui_drawer(), notificationTitle, - notificationDesc, 0); - }); + app_context_.CallInUIThread( + [imgui_drawer = imgui_drawer(), notificationTitle, notificationDesc]() { + new xe::ui::HostNotificationWindow(imgui_drawer, notificationTitle, + notificationDesc, 0); + }); } xe::threading::Sleep(delay); diff --git a/src/xenia/base/string_util.h b/src/xenia/base/string_util.h index e8f720d45..9d1bfe0a5 100644 --- a/src/xenia/base/string_util.h +++ b/src/xenia/base/string_util.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/assert.h" @@ -428,6 +429,28 @@ inline vec128_t from_string(const std::string_view value, return v; } +inline std::u16string read_u16string_and_swap(const char16_t* string_ptr) { + std::u16string input_str = std::u16string(string_ptr); + + std::u16string output_str = {}; + output_str.resize(input_str.size() + 1); + copy_and_swap_truncating(output_str.data(), input_str, input_str.size() + 1); + return output_str; +} + +inline size_t size_in_bytes(std::variant string, + bool include_terminator = true) { + if (std::holds_alternative(string)) { + return std::get(string).size() + include_terminator; + } else if (std::holds_alternative(string)) { + return (std::get(string).size() + include_terminator) * + sizeof(char16_t); + } else { + assert_always(); + } + return 0; +} + } // namespace string_util } // namespace xe diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index adb58570f..c8c0afaba 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -41,9 +41,9 @@ #include "xenia/hid/input_system.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" -#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/xam_module.h" +#include "xenia/kernel/xam/xdbf/spa_info.h" #include "xenia/kernel/xbdm/xbdm_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" #include "xenia/memory.h" @@ -1427,9 +1427,13 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, } game_config_load_callback_loop_next_index_ = SIZE_MAX; - const kernel::util::XdbfGameData db = kernel_state_->module_xdbf(module); + const auto db = kernel_state_->module_xdbf(module); - game_info_database_ = std::make_unique(&db); + game_info_database_ = + std::make_unique(db.get()); + kernel_state_->xam_state()->LoadSpaInfo(db.get()); + + kernel_state_->xam_state()->user_tracker()->AddTitleToPlayedList(); if (game_info_database_->IsValid()) { title_name_ = game_info_database_->GetTitleName( @@ -1490,17 +1494,6 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, if (!icon_block.empty()) { display_window_->SetIcon(icon_block.data(), icon_block.size()); } - - for (uint8_t slot = 0; slot < XUserMaxUserCount; slot++) { - auto user = - kernel_state_->xam_state()->profile_manager()->GetProfile(slot); - - if (user) { - kernel_state_->xam_state() - ->achievement_manager() - ->LoadTitleAchievements(user->xuid(), db); - } - } } } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 7616bea32..3cc19fbd2 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -171,6 +171,9 @@ class Emulator { patcher::PluginLoader* plugin_loader() const { return plugin_loader_.get(); } + kernel::util::GameInfoDatabase* game_info_database() const { + return game_info_database_.get(); + } // Initializes the emulator and configures all components. // The given window is used for display and the provided functions are used // to create subsystems as required. diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index aef639dda..ec817cb76 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -22,6 +22,7 @@ #include "xenia/kernel/user_module.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/xam_module.h" +#include "xenia/kernel/xam/xdbf/xdbf_io.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_memory.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_ob.h" @@ -134,11 +135,11 @@ bool KernelState::is_title_system_type(uint32_t title_id) { return (title_id >> 16) == 0xFFFE; } -util::XdbfGameData KernelState::title_xdbf() const { +const std::unique_ptr KernelState::title_xdbf() const { return module_xdbf(executable_module_); } -util::XdbfGameData KernelState::module_xdbf( +const std::unique_ptr KernelState::module_xdbf( object_ref exec_module) const { assert_not_null(exec_module); @@ -147,11 +148,32 @@ util::XdbfGameData KernelState::module_xdbf( if (XSUCCEEDED(exec_module->GetSection( fmt::format("{:08X}", exec_module->title_id()).c_str(), &resource_data, &resource_size))) { - util::XdbfGameData db(memory()->TranslateVirtual(resource_data), - resource_size); - return db; + return std::make_unique(std::span( + memory()->TranslateVirtual(resource_data), resource_size)); } - return util::XdbfGameData(nullptr, resource_size); + + return nullptr; +} + +bool KernelState::UpdateSpaData(vfs::Entry* spa_file_update) { + vfs::File* file; + if (spa_file_update->Open(vfs::FileAccess::kFileReadData, &file) != + X_STATUS_SUCCESS) { + return false; + } + + std::vector data(spa_file_update->size()); + + size_t read_bytes = 0; + if (file->ReadSync(data.data(), spa_file_update->size(), 0, &read_bytes) != + X_STATUS_SUCCESS) { + return false; + } + + xam::SpaInfo new_spa_data(std::span(data.data(), data.size())); + xam_state_->LoadSpaInfo(&new_spa_data); + emulator_->game_info_database()->Update(&new_spa_data); + return true; } uint32_t KernelState::AllocateTLS() { return uint32_t(tls_bitmap_.Acquire()); } diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index d04a4cee2..e81b92cea 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -26,12 +26,13 @@ #include "xenia/kernel/util/kernel_fwd.h" #include "xenia/kernel/util/native_list.h" #include "xenia/kernel/util/object_table.h" -#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/app_manager.h" #include "xenia/kernel/xam/content_manager.h" #include "xenia/kernel/xam/user_profile.h" #include "xenia/kernel/xam/xam_state.h" +#include "xenia/kernel/xam/xdbf/spa_info.h" +#include "xenia/kernel/xam/xdbf/xdbf_io.h" #include "xenia/kernel/xevent.h" #include "xenia/memory.h" #include "xenia/vfs/virtual_file_system.h" @@ -184,8 +185,10 @@ class KernelState { uint32_t title_id() const; static bool is_title_system_type(uint32_t title_id); - util::XdbfGameData title_xdbf() const; - util::XdbfGameData module_xdbf(object_ref exec_module) const; + const std::unique_ptr title_xdbf() const; + const std::unique_ptr module_xdbf( + object_ref exec_module) const; + bool UpdateSpaData(vfs::Entry* spa_file_update); xam::XamState* xam_state() const { return xam_state_.get(); } diff --git a/src/xenia/kernel/util/game_info_database.cc b/src/xenia/kernel/util/game_info_database.cc index 118a397b8..6053d4430 100644 --- a/src/xenia/kernel/util/game_info_database.cc +++ b/src/xenia/kernel/util/game_info_database.cc @@ -14,21 +14,24 @@ namespace xe { namespace kernel { namespace util { -GameInfoDatabase::GameInfoDatabase(const XdbfGameData* data) { +GameInfoDatabase::GameInfoDatabase(const xam::SpaInfo* data) { if (!data) { return; } - if (!data->is_valid()) { - return; - } + Init(data); +} +GameInfoDatabase::~GameInfoDatabase() {} + +void GameInfoDatabase::Init(const xam::SpaInfo* data) { + spa_gamedata_ = std::make_unique(*data); + spa_gamedata_->Load(); is_valid_ = true; - xdbf_gamedata_ = std::make_unique(*data); uint32_t compressed_size, decompressed_size = 0; const uint8_t* xlast_ptr = - xdbf_gamedata_->ReadXLast(compressed_size, decompressed_size); + spa_gamedata_->ReadXLast(compressed_size, decompressed_size); if (!xlast_ptr) { XELOGW( @@ -48,26 +51,25 @@ GameInfoDatabase::GameInfoDatabase(const XdbfGameData* data) { } } -GameInfoDatabase::~GameInfoDatabase() {} - -std::string GameInfoDatabase::GetTitleName(const XLanguage language) const { - if (!is_valid_) { - return ""; +void GameInfoDatabase::Update(const xam::SpaInfo* new_spa) { + if (*spa_gamedata_ <= *new_spa) { + return; } - return xdbf_gamedata_->title(xdbf_gamedata_->GetExistingLanguage(language)); + Init(new_spa); +} + +std::string GameInfoDatabase::GetTitleName(const XLanguage language) const { + return spa_gamedata_->title_name( + spa_gamedata_->GetExistingLanguage(language)); } std::vector GameInfoDatabase::GetIcon() const { std::vector data; - if (!is_valid_) { - return data; - } - - const XdbfBlock icon = xdbf_gamedata_->icon(); - data.resize(icon.size); - std::memcpy(data.data(), icon.buffer, icon.size); + const auto icon = spa_gamedata_->title_icon(); + data.resize(icon.size()); + std::memcpy(data.data(), icon.data(), icon.size()); return data; } @@ -76,17 +78,13 @@ XLanguage GameInfoDatabase::GetDefaultLanguage() const { return XLanguage::kEnglish; } - return xdbf_gamedata_->default_language(); + return spa_gamedata_->default_language(); } std::string GameInfoDatabase::GetLocalizedString(const uint32_t id, XLanguage language) const { - if (!is_valid_) { - return ""; - } - - return xdbf_gamedata_->GetStringTableEntry( - xdbf_gamedata_->GetExistingLanguage(language), id); + return spa_gamedata_->GetStringTableEntry( + spa_gamedata_->GetExistingLanguage(language), id); } GameInfoDatabase::Context GameInfoDatabase::GetContext( @@ -97,12 +95,15 @@ GameInfoDatabase::Context GameInfoDatabase::GetContext( return context; } - const auto xdbf_context = xdbf_gamedata_->GetContext(id); + const auto xdbf_context = spa_gamedata_->GetContext(id); + if (!xdbf_context) { + return context; + } - context.id = xdbf_context.id; - context.default_value = xdbf_context.default_value; - context.max_value = xdbf_context.max_value; - context.description = GetLocalizedString(xdbf_context.string_id); + context.id = xdbf_context->id; + context.default_value = xdbf_context->default_value; + context.max_value = xdbf_context->max_value; + context.description = GetLocalizedString(xdbf_context->string_id); return context; } @@ -114,11 +115,14 @@ GameInfoDatabase::Property GameInfoDatabase::GetProperty( return property; } - const auto xdbf_property = xdbf_gamedata_->GetProperty(id); + const auto xdbf_property = spa_gamedata_->GetProperty(id); + if (!xdbf_property) { + return property; + } - property.id = xdbf_property.id; - property.data_size = xdbf_property.data_size; - property.description = GetLocalizedString(xdbf_property.string_id); + property.id = xdbf_property->id; + property.data_size = xdbf_property->data_size; + property.description = GetLocalizedString(xdbf_property->string_id); return property; } @@ -130,17 +134,21 @@ GameInfoDatabase::Achievement GameInfoDatabase::GetAchievement( return achievement; } - const auto xdbf_achievement = xdbf_gamedata_->GetAchievement(id); + const auto xdbf_achievement = spa_gamedata_->GetAchievement(id); + if (!xdbf_achievement) { + return achievement; + } - achievement.id = xdbf_achievement.id; - achievement.image_id = xdbf_achievement.id; - achievement.gamerscore = xdbf_achievement.gamerscore; - achievement.flags = xdbf_achievement.flags; + achievement.id = xdbf_achievement->id; + achievement.image_id = xdbf_achievement->id; + achievement.gamerscore = xdbf_achievement->gamerscore; + achievement.flags = xdbf_achievement->flags; - achievement.label = GetLocalizedString(xdbf_achievement.label_id); - achievement.description = GetLocalizedString(xdbf_achievement.description_id); + achievement.label = GetLocalizedString(xdbf_achievement->label_id); + achievement.description = + GetLocalizedString(xdbf_achievement->description_id); achievement.unachieved_description = - GetLocalizedString(xdbf_achievement.unachieved_id); + GetLocalizedString(xdbf_achievement->unachieved_id); return achievement; } @@ -148,13 +156,14 @@ std::vector GameInfoDatabase::GetMatchmakingAttributes( const uint32_t id) const { std::vector result; - const auto xdbf_matchmaking_data = xdbf_gamedata_->GetMatchCollection(); + /* + const auto xdbf_matchmaking_data = spa_gamedata_->GetMatchCollection(); result.insert(result.end(), xdbf_matchmaking_data.contexts.cbegin(), xdbf_matchmaking_data.contexts.cend()); result.insert(result.end(), xdbf_matchmaking_data.properties.cbegin(), xdbf_matchmaking_data.properties.cend()); - + */ return result; } @@ -240,9 +249,9 @@ std::vector GameInfoDatabase::GetContexts() const { return contexts; } - const auto xdbf_contexts = xdbf_gamedata_->GetContexts(); + const auto xdbf_contexts = spa_gamedata_->GetContexts(); for (const auto& entry : xdbf_contexts) { - contexts.push_back(GetContext(entry.id)); + contexts.push_back(GetContext(entry->id)); } return contexts; @@ -256,9 +265,9 @@ std::vector GameInfoDatabase::GetProperties() return properties; } - const auto xdbf_properties = xdbf_gamedata_->GetProperties(); + const auto xdbf_properties = spa_gamedata_->GetProperties(); for (const auto& entry : xdbf_properties) { - properties.push_back(GetProperty(entry.id)); + properties.push_back(GetProperty(entry->id)); } return properties; @@ -272,9 +281,9 @@ std::vector GameInfoDatabase::GetAchievements() return achievements; } - const auto xdbf_achievements = xdbf_gamedata_->GetAchievements(); + const auto xdbf_achievements = spa_gamedata_->GetAchievements(); for (const auto& entry : xdbf_achievements) { - achievements.push_back(GetAchievement(entry.id)); + achievements.push_back(GetAchievement(entry->id)); } return achievements; diff --git a/src/xenia/kernel/util/game_info_database.h b/src/xenia/kernel/util/game_info_database.h index 5d3c608f6..bf2e2f88f 100644 --- a/src/xenia/kernel/util/game_info_database.h +++ b/src/xenia/kernel/util/game_info_database.h @@ -13,8 +13,8 @@ #include #include -#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/util/xlast.h" +#include "xenia/kernel/xam/xdbf/spa_info.h" namespace xe { namespace kernel { @@ -87,11 +87,12 @@ class GameInfoDatabase { // Normally titles have at least XDBF file embedded into xex. There are // certain exceptions and that's why we need to check if it is even valid. - GameInfoDatabase(const XdbfGameData* data); + GameInfoDatabase(const xam::SpaInfo* data); ~GameInfoDatabase(); bool IsValid() const { return is_valid_; } + void Update(const xam::SpaInfo* new_spa); // This is mostly extracted from XDBF. std::string GetTitleName( const XLanguage language = XLanguage::kInvalid) const; @@ -123,8 +124,10 @@ class GameInfoDatabase { std::vector GetStatsViews() const; private: + void Init(const xam::SpaInfo* data); + bool is_valid_ = false; - std::unique_ptr xdbf_gamedata_; + std::unique_ptr spa_gamedata_; std::unique_ptr xlast_gamedata_; }; diff --git a/src/xenia/kernel/util/property.cc b/src/xenia/kernel/util/property.cc index 83d32d70e..2786ddcf6 100644 --- a/src/xenia/kernel/util/property.cc +++ b/src/xenia/kernel/util/property.cc @@ -70,33 +70,34 @@ void Property::Write(Memory* memory, XUSER_PROPERTY* property) const { switch (data_type_) { case X_USER_DATA_TYPE::WSTRING: - property->data.binary.size = value_size_; + property->data.data.binary.size = value_size_; break; - case X_USER_DATA_TYPE::CONTENT: + case X_USER_DATA_TYPE::BINARY: - property->data.binary.size = value_size_; + property->data.data.binary.size = value_size_; // Property pointer must be valid at this point! - memcpy(memory->TranslateVirtual(property->data.binary.ptr), value_.data(), - value_size_); + memcpy(memory->TranslateVirtual(property->data.data.binary.ptr), + value_.data(), value_size_); break; + case X_USER_DATA_TYPE::CONTEXT: case X_USER_DATA_TYPE::INT32: - memcpy(reinterpret_cast(&property->data.s32), value_.data(), - value_size_); + memcpy(reinterpret_cast(&property->data.data.s32), + value_.data(), value_size_); break; case X_USER_DATA_TYPE::INT64: - memcpy(reinterpret_cast(&property->data.s64), value_.data(), - value_size_); + memcpy(reinterpret_cast(&property->data.data.s64), + value_.data(), value_size_); break; case X_USER_DATA_TYPE::DOUBLE: - memcpy(reinterpret_cast(&property->data.f64), value_.data(), - value_size_); + memcpy(reinterpret_cast(&property->data.data.f64), + value_.data(), value_size_); break; case X_USER_DATA_TYPE::FLOAT: - memcpy(reinterpret_cast(&property->data.f32), value_.data(), - value_size_); + memcpy(reinterpret_cast(&property->data.data.f32), + value_.data(), value_size_); break; case X_USER_DATA_TYPE::DATETIME: - memcpy(reinterpret_cast(&property->data.filetime), + memcpy(reinterpret_cast(&property->data.data.filetime), value_.data(), value_size_); break; default: @@ -106,9 +107,9 @@ void Property::Write(Memory* memory, XUSER_PROPERTY* property) const { userDataVariant Property::GetValue() const { switch (data_type_) { - case X_USER_DATA_TYPE::CONTENT: case X_USER_DATA_TYPE::BINARY: return value_; + case X_USER_DATA_TYPE::CONTEXT: case X_USER_DATA_TYPE::INT32: return *reinterpret_cast(value_.data()); case X_USER_DATA_TYPE::INT64: diff --git a/src/xenia/kernel/util/property.h b/src/xenia/kernel/util/property.h index 6a7119051..e8c921e14 100644 --- a/src/xenia/kernel/util/property.h +++ b/src/xenia/kernel/util/property.h @@ -47,8 +47,6 @@ class Property { bool RequiresPointer() const { return static_cast(property_id_.type) == - X_USER_DATA_TYPE::CONTENT || - static_cast(property_id_.type) == X_USER_DATA_TYPE::WSTRING || static_cast(property_id_.type) == X_USER_DATA_TYPE::BINARY; diff --git a/src/xenia/kernel/util/xdbf_utils.cc b/src/xenia/kernel/util/xdbf_utils.cc deleted file mode 100644 index 6fa4c8fee..000000000 --- a/src/xenia/kernel/util/xdbf_utils.cc +++ /dev/null @@ -1,419 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2016 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/kernel/util/xdbf_utils.h" -#include - -namespace xe { -namespace kernel { -namespace util { - -constexpr fourcc_t kXdbfSignatureXdbf = make_fourcc("XDBF"); -constexpr fourcc_t kXdbfSignatureXstc = make_fourcc("XSTC"); -constexpr fourcc_t kXdbfSignatureXstr = make_fourcc("XSTR"); -constexpr fourcc_t kXdbfSignatureXach = make_fourcc("XACH"); -constexpr fourcc_t kXdbfSignatureXprp = make_fourcc("XPRP"); -constexpr fourcc_t kXdbfSignatureXcxt = make_fourcc("XCXT"); -constexpr fourcc_t kXdbfSignatureXvc2 = make_fourcc("XVC2"); -constexpr fourcc_t kXdbfSignatureXmat = make_fourcc("XMAT"); -constexpr fourcc_t kXdbfSignatureXsrc = make_fourcc("XSRC"); -constexpr fourcc_t kXdbfSignatureXthd = make_fourcc("XTHD"); - -constexpr uint64_t kXdbfIdTitle = 0x8000; -constexpr uint64_t kXdbfIdXstc = 0x58535443; -constexpr uint64_t kXdbfIdXach = 0x58414348; -constexpr uint64_t kXdbfIdXprp = 0x58505250; -constexpr uint64_t kXdbfIdXctx = 0x58435854; -constexpr uint64_t kXdbfIdXvc2 = 0x58564332; -constexpr uint64_t kXdbfIdXmat = 0x584D4154; -constexpr uint64_t kXdbfIdXsrc = 0x58535243; -constexpr uint64_t kXdbfIdXthd = 0x58544844; - -XdbfWrapper::XdbfWrapper(const uint8_t* data, size_t data_size) - : data_(data), data_size_(data_size) { - if (!data || data_size <= sizeof(XbdfHeader)) { - data_ = nullptr; - return; - } - - const uint8_t* ptr = data_; - - header_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfHeader); - if (header_->magic != kXdbfSignatureXdbf) { - data_ = nullptr; - return; - } - - entries_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfEntry) * header_->entry_count; - - files_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfFileLoc) * header_->free_count; - - content_offset_ = ptr; -} - -XdbfBlock XdbfWrapper::GetEntry(XdbfSection section, uint64_t id) const { - for (uint32_t i = 0; i < header_->entry_used; ++i) { - auto& entry = entries_[i]; - if (entry.section == static_cast(section) && entry.id == id) { - XdbfBlock block; - block.buffer = content_offset_ + entry.offset; - block.size = entry.size; - return block; - } - } - return {0}; -} - -std::string XdbfWrapper::GetStringTableEntry(XLanguage language, - uint16_t string_id) const { - auto language_block = - GetEntry(XdbfSection::kStringTable, static_cast(language)); - if (!language_block) { - return ""; - } - - auto xstr_head = - reinterpret_cast(language_block.buffer); - assert_true(xstr_head->magic == kXdbfSignatureXstr); - assert_true(xstr_head->version == 1); - - const uint8_t* ptr = language_block.buffer + sizeof(XdbfSectionHeader); - const uint16_t string_count = xe::byte_swap(*(uint16_t*)ptr); - ptr += sizeof(uint16_t); - - for (uint16_t i = 0; i < string_count; ++i) { - auto entry = reinterpret_cast(ptr); - ptr += sizeof(XdbfStringTableEntry); - if (entry->id == string_id) { - return std::string(reinterpret_cast(ptr), - entry->string_length); - } - ptr += entry->string_length; - } - return ""; -} - -std::vector XdbfWrapper::GetAchievements() const { - std::vector achievements; - - auto achievement_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXach); - if (!achievement_table) { - return achievements; - } - - auto xach_head = - reinterpret_cast(achievement_table.buffer); - assert_true(xach_head->magic == kXdbfSignatureXach); - assert_true(xach_head->version == 1); - - const uint8_t* ptr = achievement_table.buffer + sizeof(XdbfSectionHeader); - const uint16_t achievement_count = xe::byte_swap(*(uint16_t*)ptr); - ptr += sizeof(uint16_t); - - for (uint16_t i = 0; i < achievement_count; ++i) { - auto entry = reinterpret_cast(ptr); - ptr += sizeof(XdbfAchievementTableEntry); - achievements.push_back(*entry); - } - return achievements; -} - -std::vector XdbfWrapper::GetProperties() const { - std::vector properties; - - auto property_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXprp); - if (!property_table) { - return properties; - } - - auto xprp_head = - reinterpret_cast(property_table.buffer); - assert_true(xprp_head->magic == kXdbfSignatureXprp); - assert_true(xprp_head->version == 1); - - const uint8_t* ptr = property_table.buffer + sizeof(XdbfSectionHeader); - const uint16_t properties_count = xe::byte_swap(*(uint16_t*)ptr); - ptr += sizeof(uint16_t); - - for (uint16_t i = 0; i < properties_count; i++) { - auto entry = reinterpret_cast(ptr); - ptr += sizeof(XdbfPropertyTableEntry); - properties.push_back(*entry); - } - return properties; -} - -std::vector XdbfWrapper::GetContexts() const { - std::vector contexts; - - auto contexts_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXctx); - if (!contexts_table) { - return contexts; - } - - auto xcxt_head = - reinterpret_cast(contexts_table.buffer); - assert_true(xcxt_head->magic == kXdbfSignatureXcxt); - assert_true(xcxt_head->version == 1); - - const uint8_t* ptr = contexts_table.buffer + sizeof(XdbfSectionHeader); - const uint32_t contexts_count = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t); - - for (uint32_t i = 0; i < contexts_count; i++) { - auto entry = reinterpret_cast(ptr); - ptr += sizeof(XdbfContextTableEntry); - contexts.push_back(*entry); - } - return contexts; -} - -std::vector XdbfWrapper::GetStatsView() const { - std::vector entries; - - auto stats_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXvc2); - if (!stats_table) { - return entries; - } - - auto xvc2_head = - reinterpret_cast(stats_table.buffer); - assert_true(xvc2_head->magic == kXdbfSignatureXvc2); - assert_true(xvc2_head->version == 1); - - const uint8_t* ptr = stats_table.buffer + sizeof(XdbfSectionHeader); - const uint16_t shared_view_count = xe::byte_swap(*(uint16_t*)ptr); - ptr += sizeof(uint16_t); - - std::map shared_view_entries; - for (uint16_t i = 0; i < shared_view_count; i++) { - uint32_t byte_count = 0; - shared_view_entries.emplace(i, GetSharedView(ptr, byte_count)); - ptr += byte_count; - } - - const uint16_t views_count = xe::byte_swap(*(uint16_t*)ptr); - ptr += sizeof(uint16_t); - - for (uint16_t i = 0; i < views_count; i++) { - auto stat = reinterpret_cast( - ptr + i * sizeof(XdbfStatsViewTableEntry)); - - XdbfViewTable table; - table.view_entry = *stat; - table.shared_view = shared_view_entries[stat->shared_index]; - entries.push_back(table); - } - - return entries; -} - -const uint8_t* XdbfWrapper::ReadXLast(uint32_t& compressed_size, - uint32_t& decompressed_size) const { - auto xlast_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXsrc); - if (!xlast_table) { - return nullptr; - } - - auto xlast_head = - reinterpret_cast(xlast_table.buffer); - assert_true(xlast_head->magic == kXdbfSignatureXsrc); - assert_true(xlast_head->version == 1); - - const uint8_t* ptr = xlast_table.buffer + sizeof(XdbfSectionHeader); - - const uint32_t filename_length = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t) + filename_length; - - decompressed_size = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t); - - compressed_size = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t); - - return ptr; -} - -XdbfTitleHeaderData XdbfWrapper::GetTitleInformation() const { - auto xlast_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXthd); - if (!xlast_table) { - return {}; - } - - auto xlast_head = - reinterpret_cast(xlast_table.buffer); - assert_true(xlast_head->magic == kXdbfSignatureXthd); - assert_true(xlast_head->version == 1); - - const XdbfTitleHeaderData* ptr = reinterpret_cast( - xlast_table.buffer + sizeof(XdbfSectionHeader)); - - return *ptr; -} - -XdbfAchievementTableEntry XdbfWrapper::GetAchievement(const uint32_t id) const { - const auto achievements = GetAchievements(); - - for (const auto& entry : achievements) { - if (entry.id != id) { - continue; - } - return entry; - } - return {}; -} - -XdbfPropertyTableEntry XdbfWrapper::GetProperty(const uint32_t id) const { - const auto properties = GetProperties(); - - for (const auto& entry : properties) { - if (entry.id != id) { - continue; - } - return entry; - } - return {}; -} - -XdbfContextTableEntry XdbfWrapper::GetContext(const uint32_t id) const { - const auto contexts = GetContexts(); - - for (const auto& entry : contexts) { - if (entry.id != id) { - continue; - } - return entry; - } - return {}; -} - -XdbfSharedView XdbfWrapper::GetSharedView(const uint8_t* ptr, - uint32_t& byte_count) const { - XdbfSharedView shared_view; - - byte_count += sizeof(XdbfSharedViewMetaTableEntry); - auto table_header = - reinterpret_cast(ptr); - ptr += sizeof(XdbfSharedViewMetaTableEntry); - - for (uint16_t i = 0; i < table_header->column_count - 1; i++) { - auto view_field = reinterpret_cast( - ptr + (i * sizeof(XdbfViewFieldEntry))); - shared_view.column_entries.push_back(*view_field); - } - - // Move pointer forward to next data - ptr += (table_header->column_count * sizeof(XdbfViewFieldEntry)); - byte_count += (table_header->column_count * sizeof(XdbfViewFieldEntry)); - - for (uint16_t i = 0; i < table_header->row_count - 1; i++) { - auto view_field = reinterpret_cast( - ptr + (i * sizeof(XdbfViewFieldEntry))); - shared_view.row_entries.push_back(*view_field); - } - - ptr += (table_header->row_count * sizeof(XdbfViewFieldEntry)); - byte_count += (table_header->row_count * sizeof(XdbfViewFieldEntry)); - - std::vector> contexts, properties; - GetPropertyBagMetadata(ptr, byte_count, contexts, properties); - - shared_view.property_bag.contexts = contexts; - shared_view.property_bag.properties = properties; - - return shared_view; -} - -void XdbfWrapper::GetPropertyBagMetadata( - const uint8_t* ptr, uint32_t& byte_count, - std::vector>& contexts, - std::vector>& properties) const { - auto xpbm_header = reinterpret_cast(ptr); - ptr += sizeof(XdbfSectionHeader); - - byte_count += sizeof(XdbfSectionHeader) + 2 * sizeof(uint32_t); - - uint32_t context_count = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t); - - uint32_t properties_count = xe::byte_swap(*(uint32_t*)ptr); - ptr += sizeof(uint32_t); - - contexts = std::vector>(context_count); - std::memcpy(contexts.data(), ptr, context_count * sizeof(uint32_t)); - - ptr += context_count * sizeof(uint32_t); - - properties = std::vector>(properties_count); - std::memcpy(properties.data(), ptr, sizeof(uint32_t) * properties_count); - - byte_count += (context_count + properties_count) * sizeof(uint32_t); -} - -XdbfPropertyBag XdbfWrapper::GetMatchCollection() const { - XdbfPropertyBag property_bag; - - auto stats_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXmat); - if (!stats_table) { - return property_bag; - } - - auto xvc2_head = - reinterpret_cast(stats_table.buffer); - assert_true(xvc2_head->magic == kXdbfSignatureXmat); - assert_true(xvc2_head->version == 1); - - const uint8_t* ptr = stats_table.buffer + sizeof(XdbfSectionHeader); - - std::vector> contexts, properties; - uint32_t byte_count = 0; - - GetPropertyBagMetadata(ptr, byte_count, contexts, properties); - - property_bag.contexts = contexts; - property_bag.properties = properties; - - return property_bag; -} - -XLanguage XdbfGameData::GetExistingLanguage(XLanguage language_to_check) const { - // A bit of a hack. Check if title in specific language exist. - // If it doesn't then for sure language is not supported. - return title(language_to_check).empty() ? default_language() - : language_to_check; -} - -XdbfBlock XdbfGameData::icon() const { - return GetEntry(XdbfSection::kImage, kXdbfIdTitle); -} - -XLanguage XdbfGameData::default_language() const { - auto block = GetEntry(XdbfSection::kMetadata, kXdbfIdXstc); - if (!block.buffer) { - return XLanguage::kEnglish; - } - auto xstc = reinterpret_cast(block.buffer); - assert_true(xstc->magic == kXdbfSignatureXstc); - return static_cast(static_cast(xstc->default_language)); -} - -std::string XdbfGameData::title() const { - return GetStringTableEntry(default_language(), kXdbfIdTitle); -} - -std::string XdbfGameData::title(XLanguage language) const { - return GetStringTableEntry(language, kXdbfIdTitle); -} - -} // namespace util -} // namespace kernel -} // namespace xe diff --git a/src/xenia/kernel/util/xdbf_utils.h b/src/xenia/kernel/util/xdbf_utils.h deleted file mode 100644 index d48b3898b..000000000 --- a/src/xenia/kernel/util/xdbf_utils.h +++ /dev/null @@ -1,250 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2016 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_KERNEL_UTIL_XDBF_UTILS_H_ -#define XENIA_KERNEL_UTIL_XDBF_UTILS_H_ - -#include -#include - -#include "xenia/base/memory.h" -#include "xenia/xbox.h" - -namespace xe { -namespace kernel { -namespace util { - -// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.h -// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.cpp - -enum class XdbfSection : uint16_t { - kMetadata = 0x0001, - kImage = 0x0002, - kStringTable = 0x0003, -}; - -#pragma pack(push, 1) -struct XbdfHeader { - xe::be magic; - xe::be version; - xe::be entry_count; - xe::be entry_used; - xe::be free_count; - xe::be free_used; -}; -static_assert_size(XbdfHeader, 24); - -struct XbdfEntry { - xe::be section; - xe::be id; - xe::be offset; - xe::be size; -}; -static_assert_size(XbdfEntry, 18); - -struct XbdfFileLoc { - xe::be offset; - xe::be size; -}; -static_assert_size(XbdfFileLoc, 8); - -struct XdbfXstc { - xe::be magic; - xe::be version; - xe::be size; - xe::be default_language; -}; -static_assert_size(XdbfXstc, 16); - -struct XdbfSectionHeader { - xe::be magic; - xe::be version; - xe::be size; -}; -static_assert_size(XdbfSectionHeader, 12); - -struct XdbfStringTableEntry { - xe::be id; - xe::be string_length; -}; -static_assert_size(XdbfStringTableEntry, 4); - -struct XdbfTitleHeaderData { - xe::be title_id; - xe::be title_type; - xe::be major; - xe::be minor; - xe::be build; - xe::be revision; - xe::be padding_0; - xe::be padding_1; - xe::be padding_2; - xe::be padding_3; -}; -static_assert_size(XdbfTitleHeaderData, 32); - -struct XdbfContextTableEntry { - xe::be id; - xe::be unk1; - xe::be string_id; - xe::be max_value; - xe::be default_value; -}; -static_assert_size(XdbfContextTableEntry, 16); - -struct XdbfPropertyTableEntry { - xe::be id; - xe::be string_id; - xe::be data_size; -}; -static_assert_size(XdbfPropertyTableEntry, 8); - -struct XdbfAchievementTableEntry { - xe::be id; - xe::be label_id; - xe::be description_id; - xe::be unachieved_id; - xe::be image_id; - xe::be gamerscore; - xe::be unkE; - xe::be flags; - xe::be unk14; - xe::be unk18; - xe::be unk1C; - xe::be unk20; -}; -static_assert_size(XdbfAchievementTableEntry, 0x24); - -struct XdbfStatsViewTableEntry { - xe::be id; - xe::be flags; - xe::be shared_index; - xe::be string_id; - xe::be unused; -}; -static_assert_size(XdbfStatsViewTableEntry, 0x10); - -struct XdbfViewFieldEntry { - xe::be size; - xe::be property_id; - xe::be flags; - xe::be attribute_id; - xe::be string_id; - xe::be aggregation_type; - xe::be ordinal; - xe::be field_type; - xe::be format_type; - xe::be unused_1; - xe::be unused_2; -}; -static_assert_size(XdbfViewFieldEntry, 0x20); - -struct XdbfSharedViewMetaTableEntry { - xe::be column_count; - xe::be row_count; - xe::be unused_1; - xe::be unused_2; -}; -static_assert_size(XdbfSharedViewMetaTableEntry, 0xC); -#pragma pack(pop) - -struct XdbfPropertyBag { - std::vector> contexts; - std::vector> properties; -}; - -struct XdbfSharedView { - std::vector column_entries; - std::vector row_entries; - XdbfPropertyBag property_bag; -}; - -struct XdbfViewTable { - XdbfStatsViewTableEntry view_entry; - XdbfSharedView shared_view; -}; - -struct XdbfBlock { - const uint8_t* buffer; - size_t size; - - operator bool() const { return buffer != nullptr; } -}; - -// Wraps an XBDF (XboxDataBaseFormat) in-memory database. -// https://free60project.github.io/wiki/XDBF.html -class XdbfWrapper { - public: - XdbfWrapper(const uint8_t* data, size_t data_size); - - // True if the target memory contains a valid XDBF instance. - bool is_valid() const { return data_ != nullptr; } - - // Gets an entry in the given section. - // If the entry is not found the returned block will be nullptr. - XdbfBlock GetEntry(XdbfSection section, uint64_t id) const; - - // Gets a string from the string table in the given language. - // Returns the empty string if the entry is not found. - std::string GetStringTableEntry(XLanguage language, uint16_t string_id) const; - std::vector GetAchievements() const; - std::vector GetProperties() const; - std::vector GetContexts() const; - - XdbfTitleHeaderData GetTitleInformation() const; - XdbfAchievementTableEntry GetAchievement(const uint32_t id) const; - XdbfPropertyTableEntry GetProperty(const uint32_t id) const; - XdbfContextTableEntry GetContext(const uint32_t id) const; - std::vector GetStatsView() const; - XdbfSharedView GetSharedView(const uint8_t* ptr, uint32_t& byte_count) const; - - void GetPropertyBagMetadata(const uint8_t* ptr, uint32_t& byte_count, - std::vector>& contexts, - std::vector>& properties) const; - - XdbfPropertyBag GetMatchCollection() const; - - const uint8_t* ReadXLast(uint32_t& compressed_size, - uint32_t& decompressed_size) const; - - private: - const uint8_t* data_ = nullptr; - size_t data_size_ = 0; - const uint8_t* content_offset_ = nullptr; - - const XbdfHeader* header_ = nullptr; - const XbdfEntry* entries_ = nullptr; - const XbdfFileLoc* files_ = nullptr; -}; - -class XdbfGameData : public XdbfWrapper { - public: - XdbfGameData(const uint8_t* data, size_t data_size) - : XdbfWrapper(data, data_size) {} - - // Checks if provided language exist, if not returns default title language. - XLanguage GetExistingLanguage(XLanguage language_to_check) const; - - // The game icon image, if found. - XdbfBlock icon() const; - - // The game's default language. - XLanguage default_language() const; - - // The game's title in its default language. - std::string title() const; - - std::string title(XLanguage language) const; -}; - -} // namespace util -} // namespace kernel -} // namespace xe - -#endif // XENIA_KERNEL_UTIL_XDBF_UTILS_H_ diff --git a/src/xenia/kernel/util/xuserdata.h b/src/xenia/kernel/util/xuserdata.h index a601c91c5..9032f2aa5 100644 --- a/src/xenia/kernel/util/xuserdata.h +++ b/src/xenia/kernel/util/xuserdata.h @@ -27,7 +27,7 @@ union AttributeKey { }; enum class X_USER_DATA_TYPE : uint8_t { - CONTENT = 0, + CONTEXT = 0, INT32 = 1, INT64 = 2, DOUBLE = 3, @@ -56,7 +56,7 @@ struct X_USER_DATA { be ptr; } binary; be filetime; - }; + } data; }; static_assert_size(X_USER_DATA, 16); @@ -96,7 +96,7 @@ class Int32UserData : public UserData { : UserData(X_USER_DATA_TYPE::INT32), value_(value) {} void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->s32 = value_; + data->data.s32 = value_; } private: @@ -109,7 +109,7 @@ class Uint32UserData : public UserData { : UserData(X_USER_DATA_TYPE::INT32), value_(value) {} void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->u32 = value_; + data->data.u32 = value_; } private: @@ -122,7 +122,7 @@ class Int64UserData : public UserData { : UserData(X_USER_DATA_TYPE::INT64), value_(value) {} void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->s64 = value_; + data->data.s64 = value_; } private: @@ -136,7 +136,7 @@ class FloatUserData : public UserData { void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->f32 = value_; + data->data.f32 = value_; } private: @@ -149,7 +149,7 @@ class DoubleUserData : public UserData { : UserData(X_USER_DATA_TYPE::DOUBLE), value_(value) {} void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->f64 = value_; + data->data.f64 = value_; } private: @@ -164,16 +164,16 @@ class UnicodeUserData : public UserData { UserData::Append(data, stream); if (value_.empty()) { - data->unicode.size = 0; - data->unicode.ptr = 0; + data->data.unicode.size = 0; + data->data.unicode.ptr = 0; return; } size_t count = value_.size() + 1; size_t size = 2 * count; assert_true(size <= std::numeric_limits::max()); - data->unicode.size = static_cast(size); - data->unicode.ptr = stream->ptr(); + data->data.unicode.size = static_cast(size); + data->data.unicode.ptr = stream->ptr(); auto buffer = reinterpret_cast(&stream->data()[stream->offset()]); stream->Advance(size); @@ -192,15 +192,15 @@ class BinaryUserData : public UserData { UserData::Append(data, stream); if (value_.empty()) { - data->binary.size = 0; - data->binary.ptr = 0; + data->data.binary.size = 0; + data->data.binary.ptr = 0; return; } size_t size = value_.size(); assert_true(size <= std::numeric_limits::max()); - data->binary.size = static_cast(size); - data->binary.ptr = stream->ptr(); + data->data.binary.size = static_cast(size); + data->data.binary.ptr = stream->ptr(); stream->Write(value_.data(), size); } @@ -221,7 +221,7 @@ class DateTimeUserData : public UserData { void Append(X_USER_DATA* data, DataByteStream* stream) override { UserData::Append(data, stream); - data->filetime = value_; + data->data.filetime = value_; } private: diff --git a/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.cc b/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.cc index 333ca574f..6019a338e 100644 --- a/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.cc +++ b/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.cc @@ -28,48 +28,45 @@ void GpdAchievementBackend::EarnAchievement(const uint64_t xuid, return; } - auto achievement = GetAchievementInfoInternal(xuid, title_id, achievement_id); - if (!achievement) { - return; - } - - XELOGI("Player: {} Unlocked Achievement: {}", user->name(), - xe::to_utf8(xe::load_and_swap( - achievement->achievement_name.c_str()))); - - const uint64_t unlock_time = Clock::QueryHostSystemTime(); - // We're adding achieved online flag because on console locally achieved - // entries don't have valid unlock time. - achievement->flags = achievement->flags | - static_cast(AchievementFlags::kAchieved) | - static_cast(AchievementFlags::kAchievedOnline); - achievement->unlock_time = unlock_time; - - SaveAchievementData(xuid, title_id, achievement_id); + kernel_state()->xam_state()->user_tracker()->UnlockAchievement( + xuid, achievement_id); } -AchievementGpdStructure* GpdAchievementBackend::GetAchievementInfoInternal( +const std::optional GpdAchievementBackend::GetAchievementInfo( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const { const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); if (!user) { - return nullptr; + return std::nullopt; } - return user->GetAchievement(title_id, achievement_id); -} + auto entry = user->games_gpd_.find(title_id); + if (entry == user->games_gpd_.cend()) { + return std::nullopt; + } -const AchievementGpdStructure* GpdAchievementBackend::GetAchievementInfo( - const uint64_t xuid, const uint32_t title_id, - const uint32_t achievement_id) const { - return GetAchievementInfoInternal(xuid, title_id, achievement_id); + const auto achievement_entry = + entry->second.GetAchievementEntry(achievement_id); + + if (!achievement_entry) { + return std::nullopt; + } + + Achievement achievement(achievement_entry); + achievement.achievement_name = + entry->second.GetAchievementTitle(achievement_id); + achievement.unlocked_description = + entry->second.GetAchievementDescription(achievement_id); + achievement.locked_description = + entry->second.GetAchievementUnachievedDescription(achievement_id); + + return achievement; } bool GpdAchievementBackend::IsAchievementUnlocked( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const { - const auto achievement = - GetAchievementInfoInternal(xuid, title_id, achievement_id); + const auto achievement = GetAchievementInfo(xuid, title_id, achievement_id); if (!achievement) { return false; @@ -79,53 +76,48 @@ bool GpdAchievementBackend::IsAchievementUnlocked( static_cast(AchievementFlags::kAchieved)) != 0; } -const std::vector* -GpdAchievementBackend::GetTitleAchievements(const uint64_t xuid, - const uint32_t title_id) const { +const std::vector GpdAchievementBackend::GetTitleAchievements( + const uint64_t xuid, const uint32_t title_id) const { const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); if (!user) { return {}; } - return user->GetTitleAchievements(title_id); + return kernel_state()->xam_state()->user_tracker()->GetUserTitleAchievements( + xuid, title_id); } -bool GpdAchievementBackend::LoadAchievementsData( - const uint64_t xuid, const util::XdbfGameData title_data) { +const std::span GpdAchievementBackend::GetAchievementIcon( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + return kernel_state()->xam_state()->user_tracker()->GetAchievementIcon( + xuid, title_id, achievement_id); +} + +bool GpdAchievementBackend::LoadAchievementsData(const uint64_t xuid) { auto user = kernel_state()->xam_state()->GetUserProfile(xuid); if (!user) { return false; } - // Question. Should loading for GPD for profile be directly done by profile or - // here? - if (!title_data.is_valid()) { - return false; - } - - const auto achievements = title_data.GetAchievements(); - if (achievements.empty()) { - return true; - } - - const auto title_id = title_data.GetTitleInformation().title_id; - - const XLanguage title_language = title_data.GetExistingLanguage( - static_cast(cvars::user_language)); - for (const auto& achievement : achievements) { - AchievementGpdStructure achievementData(title_language, title_data, - achievement); - user->achievements_[title_id].push_back(achievementData); - } - - // TODO(Gliniak): Here should be loader of GPD file for loaded title. That way - // we can load flags and unlock_time from specific user. + // GPDs are handled by UserTracker return true; } -bool GpdAchievementBackend::SaveAchievementData(const uint64_t xuid, - const uint32_t title_id, - const uint32_t achievement_id) { +bool GpdAchievementBackend::SaveAchievementData( + const uint64_t xuid, const uint32_t title_id, + const Achievement* achievement) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + // GPDs are handled by UserTracker return true; } diff --git a/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h b/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h index 6c572f121..611fedf19 100644 --- a/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h +++ b/src/xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h @@ -15,8 +15,8 @@ #include #include -#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/xam/achievement_manager.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" #include "xenia/xbox.h" namespace xe { @@ -32,25 +32,23 @@ class GpdAchievementBackend : public AchievementBackendInterface { const uint32_t achievement_id) override; bool IsAchievementUnlocked(const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const override; - const AchievementGpdStructure* GetAchievementInfo( + const std::optional GetAchievementInfo( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const override; - const std::vector* GetTitleAchievements( + const std::vector GetTitleAchievements( const uint64_t xuid, const uint32_t title_id) const override; - bool LoadAchievementsData(const uint64_t xuid, - const util::XdbfGameData title_data) override; + const std::span GetAchievementIcon( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const override; + bool LoadAchievementsData(const uint64_t xuid) override; private: - AchievementGpdStructure* GetAchievementInfoInternal( - const uint64_t xuid, const uint32_t title_id, - const uint32_t achievement_id) const; - bool SaveAchievementsData(const uint64_t xuid, const uint32_t title_id) override { return 0; }; bool SaveAchievementData(const uint64_t xuid, const uint32_t title_id, - const uint32_t achievement_id) override; + const Achievement* achievement) override; }; } // namespace xam diff --git a/src/xenia/kernel/xam/achievement_manager.cc b/src/xenia/kernel/xam/achievement_manager.cc index 79b807e68..c489a6f19 100644 --- a/src/xenia/kernel/xam/achievement_manager.cc +++ b/src/xenia/kernel/xam/achievement_manager.cc @@ -13,6 +13,7 @@ #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" #include "xenia/ui/imgui_guest_notification.h" DEFINE_bool(show_achievement_notification, false, @@ -81,27 +82,32 @@ void AchievementManager::EarnAchievement(const uint64_t xuid, // Something went really wrong! return; } - ShowAchievementEarnedNotification(achievement); + ShowAchievementEarnedNotification(&achievement.value()); } -void AchievementManager::LoadTitleAchievements( - const uint64_t xuid, const util::XdbfGameData title_data) const { - default_achievements_backend_->LoadAchievementsData(xuid, title_data); +void AchievementManager::LoadTitleAchievements(const uint64_t xuid) const { + default_achievements_backend_->LoadAchievementsData(xuid); } -const AchievementGpdStructure* AchievementManager::GetAchievementInfo( +const std::optional AchievementManager::GetAchievementInfo( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const { return default_achievements_backend_->GetAchievementInfo(xuid, title_id, achievement_id); } -const std::vector* -AchievementManager::GetTitleAchievements(const uint64_t xuid, - const uint32_t title_id) const { +const std::vector AchievementManager::GetTitleAchievements( + const uint64_t xuid, const uint32_t title_id) const { return default_achievements_backend_->GetTitleAchievements(xuid, title_id); } +const std::span AchievementManager::GetAchievementIcon( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + return default_achievements_backend_->GetAchievementIcon(xuid, title_id, + achievement_id); +} + const std::optional AchievementManager::GetTitleAchievementsInfo(const uint64_t xuid, const uint32_t title_id) const { @@ -109,13 +115,13 @@ AchievementManager::GetTitleAchievementsInfo(const uint64_t xuid, const auto achievements = GetTitleAchievements(xuid, title_id); - if (!achievements) { + if (achievements.empty()) { return std::nullopt; } - info.achievements_count = static_cast(achievements->size()); + info.achievements_count = static_cast(achievements.size()); - for (const auto& entry : *achievements) { + for (const auto& entry : achievements) { if (!entry.IsUnlocked()) { continue; } @@ -129,22 +135,15 @@ AchievementManager::GetTitleAchievementsInfo(const uint64_t xuid, bool AchievementManager::DoesAchievementExist( const uint32_t achievement_id) const { - const util::XdbfGameData title_xdbf = kernel_state()->title_xdbf(); - const util::XdbfAchievementTableEntry achievement = - title_xdbf.GetAchievement(achievement_id); - - if (!achievement.id) { - return false; - } - return true; + return kernel_state()->xam_state()->spa_info()->GetAchievement( + achievement_id); } void AchievementManager::ShowAchievementEarnedNotification( - const AchievementGpdStructure* achievement) const { + const Achievement* achievement) const { const std::string description = - fmt::format("{}G - {}", achievement->gamerscore.get(), - xe::to_utf8(xe::load_and_swap( - achievement->achievement_name.c_str()))); + fmt::format("{}G - {}", achievement->gamerscore, + xe::to_utf8(achievement->achievement_name)); const Emulator* emulator = kernel_state()->emulator(); ui::WindowedAppContext& app_context = diff --git a/src/xenia/kernel/xam/achievement_manager.h b/src/xenia/kernel/xam/achievement_manager.h index bd04d524b..203cedad3 100644 --- a/src/xenia/kernel/xam/achievement_manager.h +++ b/src/xenia/kernel/xam/achievement_manager.h @@ -16,7 +16,8 @@ #include #include "xenia/base/chrono.h" -#include "xenia/kernel/util/xdbf_utils.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" +#include "xenia/kernel/xam/xdbf/spa_info.h" #include "xenia/xbox.h" namespace xe { @@ -96,6 +97,53 @@ struct X_ACHIEVEMENT_DETAILS { }; static_assert_size(X_ACHIEVEMENT_DETAILS, 36); +// Host structures +struct AchievementDetails { + uint32_t id; + std::u16string label; + std::u16string description; + std::u16string unachieved; + uint32_t image_id; + uint32_t gamerscore; + X_ACHIEVEMENT_UNLOCK_TIME unlock_time; + uint32_t flags; + + AchievementDetails(uint32_t id, std::u16string label, + std::u16string description, std::u16string unachieved, + uint32_t image_id, uint32_t gamerscore, + X_ACHIEVEMENT_UNLOCK_TIME unlock_time, uint32_t flags) + : id(id), + label(label), + description(description), + unachieved(unachieved), + image_id(image_id), + gamerscore(gamerscore), + unlock_time(unlock_time), + flags(flags) {}; + + AchievementDetails(const AchievementTableEntry* entry, + const SpaInfo* spa_data, const XLanguage language) { + id = entry->id; + image_id = entry->image_id; + gamerscore = entry->gamerscore; + flags = entry->flags; + unlock_time = {}; + + label = + xe::to_utf16(spa_data->GetStringTableEntry(language, entry->label_id)); + description = xe::to_utf16( + spa_data->GetStringTableEntry(language, entry->description_id)); + unachieved = xe::to_utf16( + spa_data->GetStringTableEntry(language, entry->unachieved_id)); + } +}; + +struct TitleAchievementsProfileInfo { + uint32_t achievements_count; + uint32_t unlocked_achievements_count; + uint32_t gamerscore; +}; + // This is structure used inside GPD file. // GPD is writeable XDBF. // There are two info instances @@ -103,18 +151,30 @@ static_assert_size(X_ACHIEVEMENT_DETAILS, 36); // booted game (name, title_id, last boot time etc) // 2. In specific Title ID directory GPD contains there structure below for // every achievement. (unlocked or not) -struct AchievementGpdStructure { - AchievementGpdStructure(const XLanguage language, - const util::XdbfGameData xdbf, - const util::XdbfAchievementTableEntry& xdbf_entry) { - const std::string label = - xdbf.GetStringTableEntry(language, xdbf_entry.label_id); - const std::string desc = - xdbf.GetStringTableEntry(language, xdbf_entry.description_id); - const std::string locked_desc = - xdbf.GetStringTableEntry(language, xdbf_entry.unachieved_id); +struct Achievement { + Achievement() {}; + + Achievement(const X_XDBF_GPD_ACHIEVEMENT* xdbf_ach) { + if (!xdbf_ach) { + return; + } + + achievement_id = xdbf_ach->id; + image_id = xdbf_ach->image_id; + flags = xdbf_ach->flags; + gamerscore = xdbf_ach->gamerscore; + unlock_time = static_cast(xdbf_ach->unlock_time); + } + + Achievement(const XLanguage language, const SpaInfo xdbf, + const AchievementTableEntry& xdbf_entry) { + const std::string label = ""; + xdbf.GetStringTableEntry(language, xdbf_entry.label_id); + const std::string desc = ""; + xdbf.GetStringTableEntry(language, xdbf_entry.description_id); + const std::string locked_desc = ""; + xdbf.GetStringTableEntry(language, xdbf_entry.unachieved_id); - struct_size = 0x1C; achievement_id = static_cast>(xdbf_entry.id); image_id = xdbf_entry.image_id; gamerscore = static_cast>(xdbf_entry.gamerscore); @@ -128,11 +188,10 @@ struct AchievementGpdStructure { xe::load_and_swap(xe::to_utf16(locked_desc).c_str()); } - xe::be struct_size; - xe::be achievement_id; - xe::be image_id; - xe::be gamerscore; - xe::be flags; + uint32_t achievement_id; + uint32_t image_id; + uint32_t gamerscore; + uint32_t flags; X_ACHIEVEMENT_UNLOCK_TIME unlock_time; std::u16string achievement_name; std::u16string unlocked_description; @@ -144,12 +203,6 @@ struct AchievementGpdStructure { } }; -struct TitleAchievementsProfileInfo { - uint32_t achievements_count; - uint32_t unlocked_achievements_count; - uint32_t gamerscore; -}; - class AchievementBackendInterface { public: virtual ~AchievementBackendInterface() {}; @@ -161,44 +214,47 @@ class AchievementBackendInterface { const uint32_t title_id, const uint32_t achievement_id) const = 0; - virtual const AchievementGpdStructure* GetAchievementInfo( + virtual const std::optional GetAchievementInfo( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const = 0; - virtual const std::vector* GetTitleAchievements( + virtual const std::vector GetTitleAchievements( const uint64_t xuid, const uint32_t title_id) const = 0; - virtual bool LoadAchievementsData(const uint64_t xuid, - const util::XdbfGameData title_data) = 0; + virtual const std::span GetAchievementIcon( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const = 0; + virtual bool LoadAchievementsData(const uint64_t xuid) = 0; private: virtual bool SaveAchievementsData(const uint64_t xuid, const uint32_t title_id) = 0; virtual bool SaveAchievementData(const uint64_t xuid, const uint32_t title_id, - const uint32_t achievement_id) = 0; + const Achievement* achievement) = 0; }; class AchievementManager { public: AchievementManager(); - void LoadTitleAchievements(const uint64_t xuid, - const util::XdbfGameData title_id) const; + void LoadTitleAchievements(const uint64_t xuid) const; void EarnAchievement(const uint32_t user_index, const uint32_t title_id, const uint32_t achievement_id) const; void EarnAchievement(const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const; - const AchievementGpdStructure* GetAchievementInfo( + const std::optional GetAchievementInfo( const uint64_t xuid, const uint32_t title_id, const uint32_t achievement_id) const; - const std::vector* GetTitleAchievements( + const std::vector GetTitleAchievements( const uint64_t xuid, const uint32_t title_id) const; const std::optional GetTitleAchievementsInfo( const uint64_t xuid, const uint32_t title_id) const; + const std::span GetAchievementIcon( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const; private: bool DoesAchievementExist(const uint32_t achievement_id) const; - void ShowAchievementEarnedNotification( - const AchievementGpdStructure* achievement) const; + void ShowAchievementEarnedNotification(const Achievement* achievement) const; // This contains all backends with exception of default storage. std::vector> diff --git a/src/xenia/kernel/xam/apps/xgi_app.cc b/src/xenia/kernel/xam/apps/xgi_app.cc index c2fa53014..c8e7798cf 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -43,22 +43,11 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, uint32_t context_value = xe::load_and_swap(buffer + 20); XELOGD("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index, context_id, context_value); - - const util::XdbfGameData title_xdbf = kernel_state_->title_xdbf(); - if (title_xdbf.is_valid()) { - const auto context = title_xdbf.GetContext(context_id); - const XLanguage title_language = title_xdbf.GetExistingLanguage( - static_cast(XLanguage::kEnglish)); - const std::string desc = - title_xdbf.GetStringTableEntry(title_language, context.string_id); - XELOGD("XGIUserSetContextEx: {} - Set to value: {}", desc, - context_value); - - UserProfile* user_profile = - kernel_state_->xam_state()->GetUserProfile(user_index); - if (user_profile) { - user_profile->contexts_[context_id] = context_value; - } + UserProfile* user_profile = + kernel_state_->xam_state()->GetUserProfile(user_index); + if (user_profile) { + kernel_state_->xam_state()->user_tracker()->UpdateContext( + user_profile->xuid(), context_id, context_value); } return X_E_SUCCESS; } @@ -70,25 +59,14 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", user_index, property_id, value_size, value_ptr); - const util::XdbfGameData title_xdbf = kernel_state_->title_xdbf(); - if (title_xdbf.is_valid()) { - const auto property_xdbf = title_xdbf.GetProperty(property_id); - const XLanguage title_language = title_xdbf.GetExistingLanguage( - static_cast(XLanguage::kEnglish)); - const std::string desc = title_xdbf.GetStringTableEntry( - title_language, property_xdbf.string_id); + Property property(property_id, value_size, + memory_->TranslateVirtual(value_ptr)); - Property property = - Property(property_id, value_size, - memory_->TranslateVirtual(value_ptr)); - - auto user = kernel_state_->xam_state()->GetUserProfile(user_index); - if (user) { - user->AddProperty(&property); - } - XELOGD("XGIUserSetPropertyEx: Setting property: {}", desc); + auto user = kernel_state_->xam_state()->GetUserProfile(user_index); + if (user) { + kernel_state_->xam_state()->user_tracker()->AddProperty(user->xuid(), + &property); } - return X_E_SUCCESS; } case 0x000B0008: { @@ -202,9 +180,12 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, UserProfile* user_profile = kernel_state_->xam_state()->GetUserProfile(user_index); if (user_profile) { - if (user_profile->contexts_.find(context_id) != - user_profile->contexts_.cend()) { - value = user_profile->contexts_[context_id]; + auto result = + kernel_state_->xam_state()->user_tracker()->GetUserContext( + user_profile->xuid(), context_id); + + if (result) { + value = result.value(); } } xe::store_and_swap(context + 4, value); diff --git a/src/xenia/kernel/xam/content_manager.cc b/src/xenia/kernel/xam/content_manager.cc index fba2ab02a..b1262fed2 100644 --- a/src/xenia/kernel/xam/content_manager.cc +++ b/src/xenia/kernel/xam/content_manager.cc @@ -16,6 +16,7 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/filesystem.h" #include "xenia/base/string.h" +#include "xenia/emulator.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/xam/user_profile.h" #include "xenia/kernel/xfile.h" @@ -30,6 +31,7 @@ namespace xam { static const char* kThumbnailFileName = "__thumbnail.png"; static const char* kGameContentHeaderDirName = "Headers"; +static const char* kSpaFilename = "spa.bin"; static int content_device_id_ = 0; @@ -381,6 +383,15 @@ X_RESULT ContentManager::OpenContent(const std::string_view root_name, content_license = package->GetPackageLicense(); + // Check for SPA file in package. Check it only for DLCs + if (data.content_type == XContentType::kMarketplaceContent) { + std::string spa_path = fmt::format("{}:\\{}", root_name, kSpaFilename); + auto spa_update = kernel_state_->file_system()->ResolvePath(spa_path); + if (spa_update) { + kernel_state_->UpdateSpaData(spa_update); + } + } + open_packages_.insert({string_key::create(root_name), package.release()}); return X_ERROR_SUCCESS; diff --git a/src/xenia/kernel/xam/profile_manager.cc b/src/xenia/kernel/xam/profile_manager.cc index 0774aab0c..c44eb5e2f 100644 --- a/src/xenia/kernel/xam/profile_manager.cc +++ b/src/xenia/kernel/xam/profile_manager.cc @@ -104,8 +104,9 @@ void ProfileManager::EncryptAccountFile(const X_XAMACCOUNTINFO* input, enc_data_size); } -ProfileManager::ProfileManager(KernelState* kernel_state) - : kernel_state_(kernel_state) { +ProfileManager::ProfileManager(KernelState* kernel_state, + UserTracker* user_tracker) + : kernel_state_(kernel_state), user_tracker_(user_tracker) { logged_profiles_.clear(); accounts_.clear(); @@ -315,16 +316,12 @@ void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index, XELOGI("Loaded {} (GUID: {:016X}) to slot {}", profile.GetGamertagString(), xuid, assigned_user_slot); + MountProfile(xuid); + logged_profiles_[assigned_user_slot] = std::make_unique(xuid, &profile); - if (kernel_state_->emulator()->is_title_open()) { - const kernel::util::XdbfGameData db = kernel_state_->title_xdbf(); - if (db.is_valid()) { - kernel_state_->xam_state()->achievement_manager()->LoadTitleAchievements( - xuid, db); - } - } + user_tracker_->AddUser(xuid); if (notify) { kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged, @@ -338,6 +335,9 @@ void ProfileManager::Logout(const uint8_t user_index, bool notify) { if (profile == logged_profiles_.cend()) { return; } + + kernel_state_->xam_state()->user_tracker()->RemoveUser( + profile->second->xuid()); DismountProfile(profile->second->xuid()); logged_profiles_.erase(profile); if (notify) { diff --git a/src/xenia/kernel/xam/profile_manager.h b/src/xenia/kernel/xam/profile_manager.h index 7e085c257..b0d88589c 100644 --- a/src/xenia/kernel/xam/profile_manager.h +++ b/src/xenia/kernel/xam/profile_manager.h @@ -26,6 +26,14 @@ class KernelState; } // namespace kernel } // namespace xe +namespace xe { +namespace kernel { +namespace xam { +class UserTracker; +} // namespace xam +} // namespace kernel +} // namespace xe + namespace xe { namespace kernel { namespace xam { @@ -75,7 +83,7 @@ class ProfileManager { // Loading Profile means load everything // Loading Account means load basic data - ProfileManager(KernelState* kernel_state); + ProfileManager(KernelState* kernel_state, UserTracker* user_tracker); ~ProfileManager(); @@ -142,6 +150,7 @@ class ProfileManager { std::map> logged_profiles_; KernelState* kernel_state_; + UserTracker* user_tracker_; }; } // namespace xam diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index e69383efa..68104d30d 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -9,11 +9,13 @@ #include "xenia/kernel/xam/user_profile.h" +#include #include #include "third_party/fmt/include/fmt/format.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" namespace xe { namespace kernel { @@ -24,240 +26,60 @@ UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info) // 58410A1F checks the user XUID against a mask of 0x00C0000000000000 (3<<54), // if non-zero, it prevents the user from playing the game. // "You do not have permissions to perform this operation." - - // https://cs.rin.ru/forum/viewtopic.php?f=38&t=60668&hilit=gfwl+live&start=195 - // https://github.com/arkem/py360/blob/master/py360/constants.py - // XPROFILE_GAMER_YAXIS_INVERSION - AddSetting(std::make_unique(0x10040002, 0)); - // XPROFILE_OPTION_CONTROLLER_VIBRATION - AddSetting(std::make_unique(0x10040003, 3)); - // XPROFILE_GAMERCARD_ZONE - AddSetting(std::make_unique(0x10040004, 0)); - // XPROFILE_GAMERCARD_REGION - AddSetting(std::make_unique(0x10040005, 0)); - // XPROFILE_GAMERCARD_CRED - AddSetting(std::make_unique(0x10040006, 0xFA)); - // XPROFILE_OPTION_VOICE_MUTED - AddSetting(std::make_unique(0x1004000C, 3)); - // XPROFILE_OPTION_VOICE_THRU_SPEAKERS - AddSetting(std::make_unique(0x1004000D, 3)); - // XPROFILE_OPTION_VOICE_VOLUME - AddSetting(std::make_unique(0x1004000E, 0x64)); - // XPROFILE_GAMERCARD_TITLES_PLAYED - AddSetting(std::make_unique(0x10040012, 1)); - // XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED - AddSetting(std::make_unique(0x10040013, 0)); - // XPROFILE_GAMER_DIFFICULTY - AddSetting(std::make_unique(0x10040015, 0)); - // XPROFILE_GAMER_CONTROL_SENSITIVITY - AddSetting(std::make_unique(0x10040018, 0)); - // Preferred color 1 - AddSetting(std::make_unique(0x1004001D, PREFERRED_COLOR_NONE)); - // Preferred color 2 - AddSetting(std::make_unique(0x1004001E, PREFERRED_COLOR_NONE)); - // XPROFILE_GAMER_ACTION_AUTO_AIM - AddSetting(std::make_unique(0x10040022, 1)); - // XPROFILE_GAMER_ACTION_AUTO_CENTER - AddSetting(std::make_unique(0x10040023, 0)); - // XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL - AddSetting(std::make_unique(0x10040024, 0)); - // XPROFILE_GAMER_RACE_TRANSMISSION - AddSetting(std::make_unique(0x10040026, 0)); - // XPROFILE_GAMER_RACE_CAMERA_LOCATION - AddSetting(std::make_unique(0x10040027, 0)); - // XPROFILE_GAMER_RACE_BRAKE_CONTROL - AddSetting(std::make_unique(0x10040028, 0)); - // XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL - AddSetting(std::make_unique(0x10040029, 0)); - // XPROFILE_GAMERCARD_TITLE_CRED_EARNED - AddSetting(std::make_unique(0x10040038, 0)); - // XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED - AddSetting(std::make_unique(0x10040039, 0)); - - // XPROFILE_GAMERCARD_MOTTO - AddSetting(std::make_unique(0x402C0011, u"")); - // XPROFILE_GAMERCARD_PICTURE_KEY - AddSetting( - std::make_unique(0x4064000F, u"gamercard_picture_key")); - // XPROFILE_GAMERCARD_REP - AddSetting(std::make_unique(0x5004000B, 0.0f)); - - // XPROFILE_TITLE_SPECIFIC1 - AddSetting(std::make_unique(0x63E83FFF, std::vector())); - // XPROFILE_TITLE_SPECIFIC2 - AddSetting(std::make_unique(0x63E83FFE, std::vector())); - // XPROFILE_TITLE_SPECIFIC3 - AddSetting(std::make_unique(0x63E83FFD, std::vector())); + LoadProfileGpds(); } -void UserProfile::AddSetting(std::unique_ptr setting) { - UserSetting* previous_setting = setting.get(); - - std::swap(settings_[setting->GetSettingId()], previous_setting); - - if (setting->is_title_specific()) { - SaveSetting(setting.get()); +void UserProfile::LoadProfileGpds() { + // First load dashboard GPD because it stores all opened games + dashboard_gpd_ = LoadGpd(kDashboardID); + if (!dashboard_gpd_.IsValid()) { + dashboard_gpd_ = GpdInfoProfile(); } - if (previous_setting) { - // replace: swap out the old setting from the owning list - for (auto vec_it = setting_list_.begin(); vec_it != setting_list_.end(); - ++vec_it) { - if (vec_it->get() == previous_setting) { - vec_it->swap(setting); - break; - } - } - } else { - // new setting: add to the owning list - setting_list_.push_back(std::move(setting)); - } -} + const auto gpds_to_load = dashboard_gpd_.GetTitlesInfo(); -UserSetting* UserProfile::GetSetting(uint32_t setting_id) { - const auto& it = settings_.find(setting_id); - if (it == settings_.end()) { - return nullptr; - } - - UserSetting* setting = it->second; - if (setting->is_title_specific()) { - // If what we have loaded in memory isn't for the title that is running - // right now, then load it from disk. - LoadSetting(setting); - } - return setting; -} - -void UserProfile::LoadSetting(UserSetting* setting) { - if (setting->is_title_specific()) { - const std::filesystem::path content_dir = - kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_); - const std::string setting_id_str = - fmt::format("{:08X}", setting->GetSettingId()); - const std::filesystem::path file_path = content_dir / setting_id_str; - FILE* file = xe::filesystem::OpenFile(file_path, "rb"); - if (!file) { - return; - } - - const uint32_t input_file_size = - static_cast(std::filesystem::file_size(file_path)); - - if (input_file_size < sizeof(X_USER_PROFILE_SETTING_HEADER)) { - fclose(file); - // Setting seems to be invalid, remove it. - std::filesystem::remove(file_path); - return; - } - - X_USER_PROFILE_SETTING_HEADER header; - fread(&header, sizeof(X_USER_PROFILE_SETTING_HEADER), 1, file); - if (header.setting_id != setting->GetSettingId()) { - // It's setting with different ID? Corrupted perhaps. - fclose(file); - std::filesystem::remove(file_path); - return; - } - - // TODO(Gliniak): Right now we only care about CONTENT, WSTRING, BINARY - setting->SetNewSettingHeader(&header); - setting->SetNewSettingSource(X_USER_PROFILE_SETTING_SOURCE::TITLE); - std::vector serialized_data(setting->GetSettingHeader()->size); - fread(serialized_data.data(), 1, serialized_data.size(), file); - fclose(file); - setting->GetSettingData()->Deserialize(serialized_data); - } else { - // Unsupported for now. Other settings aren't per-game and need to be - // stored some other way. - XELOGW("Attempting to load unsupported profile setting 0x{:08X} from disk", - setting->GetSettingId()); - } -} - -void UserProfile::SaveSetting(UserSetting* setting) { - if (setting->is_title_specific() && - setting->GetSettingSource() == X_USER_PROFILE_SETTING_SOURCE::TITLE) { - const std::filesystem::path content_dir = - kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_); - - std::filesystem::create_directories(content_dir); - - const std::string setting_id_str = - fmt::format("{:08X}", setting->GetSettingId()); - std::filesystem::path file_path = content_dir / setting_id_str; - FILE* file = xe::filesystem::OpenFile(file_path, "wb"); - - if (!file) { - return; - } - - const std::vector serialized_setting = - setting->GetSettingData()->Serialize(); - const uint32_t serialized_setting_length = std::min( - kMaxSettingSize, static_cast(serialized_setting.size())); - - fwrite(setting->GetSettingHeader(), sizeof(X_USER_PROFILE_SETTING_HEADER), - 1, file); - // Writing data - fwrite(serialized_setting.data(), 1, serialized_setting_length, file); - fclose(file); - } else { - // Unsupported for now. Other settings aren't per-game and need to be - // stored some other way. - XELOGW("Attempting to save unsupported profile setting 0x{:08X} from disk", - setting->GetSettingId()); - } -} - -bool UserProfile::AddProperty(const Property* property) { - // Find if property already exits - Property* entry = GetProperty(property->GetPropertyId()); - if (entry) { - *entry = *property; - return true; - } - - properties_.push_back(*property); - return true; -} - -Property* UserProfile::GetProperty(const AttributeKey id) { - for (auto& entry : properties_) { - if (entry.GetPropertyId().value != id.value) { + for (const auto gpd : gpds_to_load) { + const auto gpd_data = LoadGpd(gpd->title_id); + if (gpd_data.empty()) { continue; } - return &entry; + games_gpd_.emplace(gpd->title_id, GpdInfoTitle(gpd->title_id, gpd_data)); } - - return nullptr; } -AchievementGpdStructure* UserProfile::GetAchievement(const uint32_t title_id, - const uint32_t id) { - auto title_achievements = achievements_.find(title_id); - if (title_achievements == achievements_.end()) { - return nullptr; +std::vector UserProfile::LoadGpd(const uint32_t title_id) { + auto entry = kernel_state()->file_system()->ResolvePath( + fmt::format("{:016X}:\\{:08X}.gpd", xuid_, title_id)); + + if (!entry) { + XELOGW("User {} (XUID: {:016X}) doesn't have profile GPD!", name(), xuid()); + return {}; } - for (auto& entry : title_achievements->second) { - if (entry.achievement_id == id) { - return &entry; - } + vfs::File* file; + auto result = entry->Open(vfs::FileAccess::kFileReadData, &file); + if (result != X_STATUS_SUCCESS) { + XELOGW("User {} (XUID: {:016X}) cannot open profile GPD!", name(), xuid()); + return {}; } - return nullptr; + + std::vector data(entry->size()); + + size_t read_size = 0; + result = file->ReadSync(data.data(), entry->size(), 0, &read_size); + if (result != X_STATUS_SUCCESS || read_size != entry->size()) { + XELOGW( + "User {} (XUID: {:016X}) cannot read profile GPD! Status: {:08X} read: " + "{}/{} bytes", + name(), xuid(), result, read_size, entry->size()); + return {}; + } + + return data; } -std::vector* UserProfile::GetTitleAchievements( - const uint32_t title_id) { - auto title_achievements = achievements_.find(title_id); - if (title_achievements == achievements_.end()) { - return nullptr; - } - - return &title_achievements->second; -} +bool UserProfile::WriteGpd(const uint32_t title_id) { return false; } } // namespace xam } // namespace kernel diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 448698af3..63e270f89 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -16,63 +16,25 @@ #include #include -#include "xenia/base/byte_stream.h" #include "xenia/kernel/util/property.h" #include "xenia/kernel/util/xuserdata.h" -#include "xenia/kernel/xam/achievement_manager.h" +#include "xenia/kernel/xam/xdbf/gpd_info_profile.h" +#include "xenia/kernel/xam/xdbf/gpd_info_title.h" #include "xenia/xbox.h" namespace xe { namespace kernel { namespace xam { -constexpr uint32_t kMaxSettingSize = 0x03E8; - enum class X_USER_PROFILE_SETTING_SOURCE : uint32_t { NOT_SET = 0, - DEFAULT = 1, - TITLE = 2, + DEFAULT = 1, // Default value taken from default OS values. + TITLE = 2, // Value written by title or OS. UNKNOWN = 3, }; -enum PREFERRED_COLOR_OPTIONS : uint32_t { - PREFERRED_COLOR_NONE, - PREFERRED_COLOR_BLACK, - PREFERRED_COLOR_WHITE, - PREFERRED_COLOR_YELLOW, - PREFERRED_COLOR_ORANGE, - PREFERRED_COLOR_PINK, - PREFERRED_COLOR_RED, - PREFERRED_COLOR_PURPLE, - PREFERRED_COLOR_BLUE, - PREFERRED_COLOR_GREEN, - PREFERRED_COLOR_BROWN, - PREFERRED_COLOR_SILVER -}; - -// Each setting contains 0x18 bytes long header -struct X_USER_PROFILE_SETTING_HEADER { - xe::be setting_id; - xe::be unknown_1; - xe::be setting_type; - char unknown_2[3]; - xe::be unknown_3; - - union { - // Size is used only for types: CONTENT, WSTRING, BINARY - be size; - // Raw values that can be written. They do not need to be serialized. - be s32; - be s64; - be u32; - be f64; - be f32; - }; -}; -static_assert_size(X_USER_PROFILE_SETTING_HEADER, 0x18); - struct X_USER_PROFILE_SETTING { - xe::be from; + xe::be source; union { xe::be user_index; xe::be xuid; @@ -85,88 +47,6 @@ struct X_USER_PROFILE_SETTING { }; static_assert_size(X_USER_PROFILE_SETTING, 40); -class UserSetting { - public: - template - UserSetting(uint32_t setting_id, T data) { - header_.setting_id = setting_id; - - setting_id_.value = setting_id; - CreateUserData(setting_id, data); - } - - static bool is_title_specific(uint32_t setting_id) { - return (setting_id & 0x3F00) == 0x3F00; - } - - bool is_title_specific() const { - return is_title_specific(setting_id_.value); - } - - const uint32_t GetSettingId() const { return setting_id_.value; } - const X_USER_PROFILE_SETTING_SOURCE GetSettingSource() const { - return created_by_; - } - const X_USER_PROFILE_SETTING_HEADER* GetSettingHeader() const { - return &header_; - } - UserData* GetSettingData() { return user_data_.get(); } - - void SetNewSettingSource(X_USER_PROFILE_SETTING_SOURCE new_source) { - created_by_ = new_source; - } - - void SetNewSettingHeader(X_USER_PROFILE_SETTING_HEADER* header) { - header_ = *header; - } - - private: - void CreateUserData(uint32_t setting_id, uint32_t data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::INT32); - header_.u32 = data; - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, int32_t data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::INT32); - header_.s32 = data; - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, float data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::FLOAT); - header_.f32 = data; - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, double data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::DOUBLE); - header_.f64 = data; - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, int64_t data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::INT64); - header_.s64 = data; - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, const std::u16string& data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::WSTRING); - header_.size = - std::min(kMaxSettingSize, static_cast((data.size() + 1) * 2)); - user_data_ = std::make_unique(data); - } - void CreateUserData(uint32_t setting_id, const std::vector& data) { - header_.setting_type = static_cast(X_USER_DATA_TYPE::BINARY); - header_.size = - std::min(kMaxSettingSize, static_cast(data.size())); - user_data_ = std::make_unique(data); - } - - X_USER_PROFILE_SETTING_SOURCE created_by_ = - X_USER_PROFILE_SETTING_SOURCE::DEFAULT; - - X_USER_PROFILE_SETTING_HEADER header_ = {}; - AttributeKey setting_id_ = {}; - std::unique_ptr user_data_ = nullptr; -}; - class UserProfile { public: UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info); @@ -185,34 +65,22 @@ class UserProfile { sizeof(account_info_.passcode)); }; - void AddSetting(std::unique_ptr setting); - UserSetting* GetSetting(uint32_t setting_id); - - bool AddProperty(const Property* property); - Property* GetProperty(const AttributeKey id); - - std::map contexts_; - + friend class UserTracker; friend class GpdAchievementBackend; - protected: - AchievementGpdStructure* GetAchievement(const uint32_t title_id, - const uint32_t id); - std::vector* GetTitleAchievements( - const uint32_t title_id); - private: uint64_t xuid_; X_XAMACCOUNTINFO account_info_; - std::vector> setting_list_; - std::unordered_map settings_; - std::map> achievements_; + GpdInfoProfile dashboard_gpd_; + std::map games_gpd_; + std::map contexts_; std::vector properties_; - void LoadSetting(UserSetting*); - void SaveSetting(UserSetting*); + void LoadProfileGpds(); + std::vector LoadGpd(const uint32_t title_id); + bool WriteGpd(const uint32_t title_id); }; } // namespace xam diff --git a/src/xenia/kernel/xam/user_settings.cc b/src/xenia/kernel/xam/user_settings.cc new file mode 100644 index 000000000..fa47826f0 --- /dev/null +++ b/src/xenia/kernel/xam/user_settings.cc @@ -0,0 +1,190 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include + +#include "xenia/kernel/xam/user_settings.h" + +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" + +namespace xe { +namespace kernel { +namespace xam { + +UserSetting::UserSetting(UserSettingId setting_id, SettingTypes setting_data) + : setting_id_(setting_id), + setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) { + setting_type_ = get_setting_type(static_cast(setting_id)); + max_size_ = get_setting_max_size(static_cast(setting_id)); + user_data_ = {}; + + user_data_.type = setting_type_; + + switch (setting_type_) { + case X_USER_DATA_TYPE::BINARY: + extended_data_ = std::get>(setting_data); + size_ = static_cast(extended_data_.size()); + user_data_.data.binary.size = size_; + break; + case X_USER_DATA_TYPE::WSTRING: { + std::u16string str = std::get(setting_data); + size_ = static_cast(string_util::size_in_bytes(str)); + user_data_.data.unicode.size = size_; + + extended_data_.resize(size_); + memcpy(extended_data_.data(), reinterpret_cast(str.data()), + size_); + break; + } + case X_USER_DATA_TYPE::INT32: + user_data_.data.s32 = std::get(setting_data); + size_ = sizeof(int32_t); + break; + case X_USER_DATA_TYPE::FLOAT: + user_data_.data.f32 = std::get(setting_data); + size_ = sizeof(int32_t); + break; + case X_USER_DATA_TYPE::CONTEXT: + user_data_.data.s32 = std::get(setting_data); + size_ = sizeof(int32_t); + break; + case X_USER_DATA_TYPE::DOUBLE: + user_data_.data.f64 = std::get(setting_data); + size_ = sizeof(int64_t); + break; + case X_USER_DATA_TYPE::DATETIME: + case X_USER_DATA_TYPE::INT64: + user_data_.data.s64 = std::get(setting_data); + size_ = sizeof(int64_t); + break; + default: + assert_always(); + } +} + +UserSetting::UserSetting(const X_USER_PROFILE_SETTING* profile_setting) + : setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) { + setting_id_ = static_cast(profile_setting->setting_id.get()); + setting_type_ = get_setting_type(static_cast(setting_id_)); + max_size_ = get_setting_max_size(static_cast(setting_id_)); + size_ = static_cast(get_setting_data_size(profile_setting)); + + // Set that union to zero + user_data_.data.s64 = 0; + + switch (setting_type_) { + case X_USER_DATA_TYPE::WSTRING: + case X_USER_DATA_TYPE::BINARY: { + user_data_.data.binary.size = profile_setting->data.data.binary.size; + + extended_data_.resize(profile_setting->data.data.binary.size); + memcpy(extended_data_.data(), + kernel_memory()->TranslateVirtual( + profile_setting->data.data.binary.ptr), + size_); + break; + } + case X_USER_DATA_TYPE::INT32: + user_data_.data.s32 = profile_setting->data.data.s32; + break; + case X_USER_DATA_TYPE::FLOAT: + user_data_.data.f32 = profile_setting->data.data.f32; + break; + case X_USER_DATA_TYPE::CONTEXT: + user_data_.data.u32 = profile_setting->data.data.u32; + break; + case X_USER_DATA_TYPE::DATETIME: + user_data_.data.filetime = profile_setting->data.data.filetime; + break; + case X_USER_DATA_TYPE::DOUBLE: + user_data_.data.f64 = profile_setting->data.data.f64; + break; + case X_USER_DATA_TYPE::INT64: + user_data_.data.s64 = profile_setting->data.data.s64; + break; + default: + assert_always(); + } +} + +UserSetting::UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting, + std::span extended_data) + : setting_source_(X_USER_PROFILE_SETTING_SOURCE::TITLE) { + setting_id_ = static_cast(profile_setting->setting_id.get()); + setting_type_ = get_setting_type(static_cast(setting_id_)); + max_size_ = get_setting_max_size(static_cast(setting_id_)); + size_ = 0; + + user_data_ = {}; + user_data_.type = setting_type_; + + switch (setting_type_) { + case X_USER_DATA_TYPE::WSTRING: + case X_USER_DATA_TYPE::BINARY: { + user_data_.data.binary.ptr = 0; + size_ = profile_setting->base_data.size; + user_data_.data.binary.size = size_; + + extended_data_.resize(size_); + const uint8_t* ptr = reinterpret_cast(profile_setting) + + sizeof(X_XDBF_GPD_SETTING_HEADER); + memcpy(extended_data_.data(), ptr, size_); + break; + } + + case X_USER_DATA_TYPE::INT32: + user_data_.data.s32 = profile_setting->base_data.s32; + break; + case X_USER_DATA_TYPE::FLOAT: + user_data_.data.f32 = profile_setting->base_data.f32; + break; + case X_USER_DATA_TYPE::CONTEXT: + user_data_.data.u32 = profile_setting->base_data.u32; + break; + case X_USER_DATA_TYPE::INT64: + case X_USER_DATA_TYPE::DATETIME: + user_data_.data.s64 = profile_setting->base_data.s64; + break; + case X_USER_DATA_TYPE::DOUBLE: + user_data_.data.f64 = profile_setting->base_data.f64; + break; + default: + assert_always(); + } +} + +std::vector UserSetting::serialize_to_gpd() const { + std::vector data(sizeof(X_XDBF_GPD_SETTING_HEADER) + + extended_data_.size()); + + X_XDBF_GPD_SETTING_HEADER header = {}; + + header.setting_id = static_cast(setting_id_); + header.setting_type = setting_type_; + + memcpy(&header.base_data, &user_data_.data, 8); + + // Copy header to vector + memcpy(data.data(), &header, sizeof(X_XDBF_GPD_SETTING_HEADER)); + + memcpy(data.data() + sizeof(X_XDBF_GPD_SETTING_HEADER), extended_data_.data(), + extended_data_.size()); + + return data; +} + +const X_USER_DATA* UserSetting::get_base_data() { return &user_data_; } + +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/user_settings.h b/src/xenia/kernel/xam/user_settings.h new file mode 100644 index 000000000..d16c3d691 --- /dev/null +++ b/src/xenia/kernel/xam/user_settings.h @@ -0,0 +1,502 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_USER_SETTINGS_H_ +#define XENIA_KERNEL_XAM_USER_SETTINGS_H_ + +#include +#include +#include +#include + +#include "xenia/kernel/util/xuserdata.h" +#include "xenia/kernel/xam/profile_manager.h" +#include "xenia/kernel/xam/user_profile.h" + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +constexpr uint32_t kMaxSettingSize = 0x03E8; + +constexpr uint32_t SettingKey(X_USER_DATA_TYPE type, uint16_t size, + uint16_t id) { + return static_cast(type) << 28 | size << 16 | id; +} + +enum class UserSettingId : uint32_t { + XPROFILE_PERMISSIONS = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0), + XPROFILE_GAMER_TYPE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 1), // 0x10040001, + XPROFILE_GAMER_YAXIS_INVERSION = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 2), // 0x10040002, + XPROFILE_OPTION_CONTROLLER_VIBRATION = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 3), // 0x10040003, + XPROFILE_GAMERCARD_ZONE = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 4), // 0x10040004, + XPROFILE_GAMERCARD_REGION = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 5), // 0x10040005, + XPROFILE_GAMERCARD_CRED = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 6), // 0x10040006, + XPROFILE_GAMER_PRESENCE_USER_STATE = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 7), // 0x10040007, + XPROFILE_GAMERCARD_HAS_VISION = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 8), // 0x10040008, + + XPROFILE_OPTION_VOICE_MUTED = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xC), // 0x1004000C, + XPROFILE_OPTION_VOICE_THRU_SPEAKERS = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xD), // 0x1004000D, + XPROFILE_OPTION_VOICE_VOLUME = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xE), // 0x1004000E, + + XPROFILE_GAMERCARD_TITLES_PLAYED = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x12), // 0x10040012, + XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x13), // 0x10040013, + XPROFILE_GAMER_DIFFICULTY = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x15), // 0x10040015, + XPROFILE_GAMER_CONTROL_SENSITIVITY = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x18), // 0x10040018, + XPROFILE_GAMER_PREFERRED_COLOR_FIRST = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x1D), // 0x1004001D, + XPROFILE_GAMER_PREFERRED_COLOR_SECOND = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x1E), // 0x1004001E, + XPROFILE_GAMER_ACTION_AUTO_AIM = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x22), // 0x10040022, + XPROFILE_GAMER_ACTION_AUTO_CENTER = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x23), // 0x10040023, + XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x24), // 0x10040024, + XPROFILE_GAMER_RACE_TRANSMISSION = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x26), // 0x10040026, + XPROFILE_GAMER_RACE_CAMERA_LOCATION = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x27), // 0x10040027, + XPROFILE_GAMER_RACE_BRAKE_CONTROL = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x28), // 0x10040028, + XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x29), // 0x10040029, + XPROFILE_GAMERCARD_TITLE_CRED_EARNED = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x38), // 0x10040038, + XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x39), // 0x10040039, + XPROFILE_GAMER_TIER = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x3A), // 0x1004003A, + XPROFILE_MESSENGER_SIGNUP_STATE = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3B), // 0x1004003B, + XPROFILE_MESSENGER_AUTO_SIGNIN = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3C), // 0x1004003C, + XPROFILE_SAVE_WINDOWS_LIVE_PASSWORD = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3D), // 0x1004003D, + XPROFILE_FRIENDSAPP_SHOW_BUDDIES = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3E), // 0x1004003E, + XPROFILE_GAMERCARD_SERVICE_TYPE_FLAGS = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F), // 0x1004003F, + XPROFILE_TENURE_LEVEL = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x47), // 0x10040047, + XPROFILE_TENURE_MILESTONE = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x48), // 0x10040048, + + XPROFILE_SUBSCRIPTION_TYPE_LENGTH_IN_MONTHS = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4B), // 0x1004004B, + XPROFILE_SUBSCRIPTION_PAYMENT_TYPE = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4C), // 0x1004004C, + XPROFILE_PEC_INFO = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x4D), // 0x1004004D, + XPROFILE_NUI_BIOMETRIC_SIGNIN = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x4E), // 0x1004004E, set by XamUserNuiEnableBiometric + XPROFILE_GFWL_VADNORMAL = SettingKey(X_USER_DATA_TYPE::INT32, + sizeof(uint32_t), 0x4F), // 0x1004004F, + XPROFILE_BEACONS_SOCIAL_NETWORK_SHARING = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x52), // 0x10040052, + XPROFILE_USER_PREFERENCES = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x53), // 0x10040053, + XPROFILE_XBOXONE_GAMERSCORE = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x57), // 0x10040057, "XboxOneGamerscore" inside dash.xex + + WEB_EMAIL_FORMAT = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2000), // 0x10042000, + WEB_FLAGS = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2001), // 0x10042001, + WEB_SPAM = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2002), // 0x10042002, + WEB_FAVORITE_GENRE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2003), // 0x10042003, + WEB_FAVORITE_GAME = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2004), // 0x10042004, + WEB_FAVORITE_GAME1 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2005), // 0x10042005, + WEB_FAVORITE_GAME2 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2006), // 0x10042006, + WEB_FAVORITE_GAME3 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2007), // 0x10042007, + WEB_FAVORITE_GAME4 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2008), // 0x10042008, + WEB_FAVORITE_GAME5 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x2009), // 0x10042009, + WEB_PLATFORMS_OWNED = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x200A), // 0x1004200A, + WEB_CONNECTION_SPEED = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x200B), // 0x1004200B, + WEB_FLASH = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x200C), // 0x1004200C, + WEB_VIDEO_PREFERENCE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x200D), // 0x1004200D, + XPROFILE_CRUX_MEDIA_STYLE1 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EA), // 0x100403EA, + XPROFILE_CRUX_MEDIA_STYLE2 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EB), // 0x100403EB, + XPROFILE_CRUX_MEDIA_STYLE3 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EC), // 0x100403EC, + XPROFILE_CRUX_TOP_ALBUM1 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3ED), // 0x100403ED, + XPROFILE_CRUX_TOP_ALBUM2 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EE), // 0x100403EE, + XPROFILE_CRUX_TOP_ALBUM3 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EF), // 0x100403EF, + XPROFILE_CRUX_TOP_ALBUM4 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F0), // 0x100403F0, + XPROFILE_CRUX_TOP_ALBUM5 = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F1), // 0x100403F1, + XPROFILE_CRUX_BKGD_IMAGE = SettingKey( + X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F3), // 0x100403F3, + + XPROFILE_GAMERCARD_USER_LOCATION = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x52, 0x41), // 0x40520041, + + XPROFILE_GAMERCARD_USER_NAME = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x104, 0x40), // 0x41040040, + + XPROFILE_GAMERCARD_USER_URL = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x190, 0x42), // 0x41900042, + XPROFILE_GAMERCARD_USER_BIO = SettingKey( + X_USER_DATA_TYPE::WSTRING, kMaxSettingSize, 0x43), // 0x43E80043, + + XPROFILE_CRUX_BIO = SettingKey(X_USER_DATA_TYPE::WSTRING, kMaxSettingSize, + 0x3FA), // 0x43E803FA, + XPROFILE_CRUX_BG_SMALL_PRIVATE = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FB), // 0x406403FB, + XPROFILE_CRUX_BG_LARGE_PRIVATE = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FC), // 0x406403FC, + XPROFILE_CRUX_BG_SMALL_PUBLIC = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FD), // 0x406403FD, + XPROFILE_CRUX_BG_LARGE_PUBLIC = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FE), // 0x406403FE + + XPROFILE_GAMERCARD_PICTURE_KEY = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0xF), // 0x4064000F, + XPROFILE_GAMERCARD_PERSONAL_PICTURE = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x10), // 0x40640010, + XPROFILE_GAMERCARD_MOTTO = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x2C, 0x11), // 0x402C0011, + XPROFILE_GFWL_RECDEVICEDESC = + SettingKey(X_USER_DATA_TYPE::WSTRING, 200, 0x49), // 0x40C80049, + + XPROFILE_GFWL_PLAYDEVICEDESC = + SettingKey(X_USER_DATA_TYPE::WSTRING, 200, 0x4B), // 0x40C8004B, + XPROFILE_CRUX_MEDIA_PICTURE = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3E8), // 0x406403E8, + XPROFILE_CRUX_MEDIA_MOTTO = + SettingKey(X_USER_DATA_TYPE::WSTRING, 0x100, 0x3F6), // 0x410003F6, + + XPROFILE_GAMERCARD_REP = + SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float), 0xB), // 0x5004000B, + XPROFILE_GFWL_VOLUMELEVEL = + SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float), 0x4C), // 0x5004004C, + + XPROFILE_GFWL_RECLEVEL = SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float), + 0x4D), // 0x5004004D, + XPROFILE_GFWL_PLAYDEVICE = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x4A), // 0x6010004A, + + XPROFILE_VIDEO_METADATA = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x20, 0x4A), // 0x6020004A, + + XPROFILE_CRUX_OFFLINE_ID = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x34, 0x3F2), // 0x603403F2, + + XPROFILE_UNK_61180050 = + SettingKey(X_USER_DATA_TYPE::BINARY, 280, 0x50), // 0x61180050, + + XPROFILE_JUMP_IN_LIST = SettingKey(X_USER_DATA_TYPE::BINARY, kMaxSettingSize, + 0x51), // 0x63E80051, + + XPROFILE_GAMERCARD_PARTY_ADDR = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x62, 0x54), // 0x60620054, + + XPROFILE_CRUX_TOP_MUSIC = + SettingKey(X_USER_DATA_TYPE::BINARY, 0xA8, 0x3F5), // 0x60A803F5, + + XPROFILE_CRUX_TOP_MEDIAID1 = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F7), // 0x601003F7, + XPROFILE_CRUX_TOP_MEDIAID2 = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F8), // 0x601003F8, + XPROFILE_CRUX_TOP_MEDIAID3 = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F9), // 0x601003F9, + + XPROFILE_GAMERCARD_AVATAR_INFO_1 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x44), // 0x63E80044, + XPROFILE_GAMERCARD_AVATAR_INFO_2 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x45), // 0x63E80045, + XPROFILE_GAMERCARD_PARTY_INFO = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x100, 0x46), // 0x61000046, + + XPROFILE_TITLE_SPECIFIC1 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFF), // 0x63E83FFF, + XPROFILE_TITLE_SPECIFIC2 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFE), // 0x63E83FFE, + XPROFILE_TITLE_SPECIFIC3 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFD), // 0x63E83FFD, + + XPROFILE_CRUX_LAST_CHANGE_TIME = SettingKey( + X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t), 0x3F4), // 0x700803F4, + XPROFILE_TENURE_NEXT_MILESTONE_DATE = + SettingKey(X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t), + 0x49), // 0x70080049, aka ProfileDateTimeCreated? + XPROFILE_LAST_LIVE_SIGNIN = + SettingKey(X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t), + 0x4F), // 0x7008004F, named "LastOnLIVE" in Velocity +}; + +constexpr static std::array known_settings = { + UserSettingId::XPROFILE_PERMISSIONS, + UserSettingId::XPROFILE_GAMER_TYPE, + UserSettingId::XPROFILE_GAMER_YAXIS_INVERSION, + UserSettingId::XPROFILE_OPTION_CONTROLLER_VIBRATION, + UserSettingId::XPROFILE_GAMERCARD_ZONE, + UserSettingId::XPROFILE_GAMERCARD_REGION, + UserSettingId::XPROFILE_GAMERCARD_CRED, + UserSettingId::XPROFILE_GAMER_PRESENCE_USER_STATE, + UserSettingId::XPROFILE_GAMERCARD_HAS_VISION, + UserSettingId::XPROFILE_OPTION_VOICE_MUTED, + UserSettingId::XPROFILE_OPTION_VOICE_THRU_SPEAKERS, + UserSettingId::XPROFILE_OPTION_VOICE_VOLUME, + UserSettingId::XPROFILE_GAMERCARD_TITLES_PLAYED, + UserSettingId::XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED, + UserSettingId::XPROFILE_GAMER_DIFFICULTY, + UserSettingId::XPROFILE_GAMER_CONTROL_SENSITIVITY, + UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_FIRST, + UserSettingId::XPROFILE_GAMER_PREFERRED_COLOR_SECOND, + UserSettingId::XPROFILE_GAMER_ACTION_AUTO_AIM, + UserSettingId::XPROFILE_GAMER_ACTION_AUTO_CENTER, + UserSettingId::XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL, + UserSettingId::XPROFILE_GAMER_RACE_TRANSMISSION, + UserSettingId::XPROFILE_GAMER_RACE_CAMERA_LOCATION, + UserSettingId::XPROFILE_GAMER_RACE_BRAKE_CONTROL, + UserSettingId::XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL, + UserSettingId::XPROFILE_GAMERCARD_TITLE_CRED_EARNED, + UserSettingId::XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED, + UserSettingId::XPROFILE_GAMER_TIER, + UserSettingId::XPROFILE_MESSENGER_SIGNUP_STATE, + UserSettingId::XPROFILE_MESSENGER_AUTO_SIGNIN, + UserSettingId::XPROFILE_SAVE_WINDOWS_LIVE_PASSWORD, + UserSettingId::XPROFILE_FRIENDSAPP_SHOW_BUDDIES, + UserSettingId::XPROFILE_GAMERCARD_SERVICE_TYPE_FLAGS, + UserSettingId::XPROFILE_TENURE_LEVEL, + UserSettingId::XPROFILE_TENURE_MILESTONE, + UserSettingId::XPROFILE_SUBSCRIPTION_TYPE_LENGTH_IN_MONTHS, + UserSettingId::XPROFILE_SUBSCRIPTION_PAYMENT_TYPE, + UserSettingId::XPROFILE_PEC_INFO, + UserSettingId::XPROFILE_NUI_BIOMETRIC_SIGNIN, + UserSettingId::XPROFILE_GFWL_VADNORMAL, + UserSettingId::XPROFILE_BEACONS_SOCIAL_NETWORK_SHARING, + UserSettingId::XPROFILE_USER_PREFERENCES, + UserSettingId::XPROFILE_XBOXONE_GAMERSCORE, + UserSettingId::WEB_EMAIL_FORMAT, + UserSettingId::WEB_FLAGS, + UserSettingId::WEB_SPAM, + UserSettingId::WEB_FAVORITE_GENRE, + UserSettingId::WEB_FAVORITE_GAME, + UserSettingId::WEB_FAVORITE_GAME1, + UserSettingId::WEB_FAVORITE_GAME2, + UserSettingId::WEB_FAVORITE_GAME3, + UserSettingId::WEB_FAVORITE_GAME4, + UserSettingId::WEB_FAVORITE_GAME5, + UserSettingId::WEB_PLATFORMS_OWNED, + UserSettingId::WEB_CONNECTION_SPEED, + UserSettingId::WEB_FLASH, + UserSettingId::WEB_VIDEO_PREFERENCE, + UserSettingId::XPROFILE_CRUX_MEDIA_STYLE1, + UserSettingId::XPROFILE_CRUX_MEDIA_STYLE2, + UserSettingId::XPROFILE_CRUX_MEDIA_STYLE3, + UserSettingId::XPROFILE_CRUX_TOP_ALBUM1, + UserSettingId::XPROFILE_CRUX_TOP_ALBUM2, + UserSettingId::XPROFILE_CRUX_TOP_ALBUM3, + UserSettingId::XPROFILE_CRUX_TOP_ALBUM4, + UserSettingId::XPROFILE_CRUX_TOP_ALBUM5, + UserSettingId::XPROFILE_CRUX_BKGD_IMAGE, + UserSettingId::XPROFILE_GAMERCARD_USER_LOCATION, + UserSettingId::XPROFILE_GAMERCARD_USER_NAME, + UserSettingId::XPROFILE_GAMERCARD_USER_URL, + UserSettingId::XPROFILE_GAMERCARD_USER_BIO, + UserSettingId::XPROFILE_CRUX_BIO, + UserSettingId::XPROFILE_CRUX_BG_SMALL_PRIVATE, + UserSettingId::XPROFILE_CRUX_BG_LARGE_PRIVATE, + UserSettingId::XPROFILE_CRUX_BG_SMALL_PUBLIC, + UserSettingId::XPROFILE_CRUX_BG_LARGE_PUBLIC, + UserSettingId::XPROFILE_GAMERCARD_PICTURE_KEY, + UserSettingId::XPROFILE_GAMERCARD_PERSONAL_PICTURE, + UserSettingId::XPROFILE_GAMERCARD_MOTTO, + UserSettingId::XPROFILE_GFWL_RECDEVICEDESC, + UserSettingId::XPROFILE_GFWL_PLAYDEVICEDESC, + UserSettingId::XPROFILE_CRUX_MEDIA_PICTURE, + UserSettingId::XPROFILE_CRUX_MEDIA_MOTTO, + UserSettingId::XPROFILE_GAMERCARD_REP, + UserSettingId::XPROFILE_GFWL_VOLUMELEVEL, + UserSettingId::XPROFILE_GFWL_RECLEVEL, + UserSettingId::XPROFILE_GFWL_PLAYDEVICE, + UserSettingId::XPROFILE_VIDEO_METADATA, + UserSettingId::XPROFILE_CRUX_OFFLINE_ID, + UserSettingId::XPROFILE_UNK_61180050, + UserSettingId::XPROFILE_JUMP_IN_LIST, + UserSettingId::XPROFILE_GAMERCARD_PARTY_ADDR, + UserSettingId::XPROFILE_CRUX_TOP_MUSIC, + UserSettingId::XPROFILE_CRUX_TOP_MEDIAID1, + UserSettingId::XPROFILE_CRUX_TOP_MEDIAID2, + UserSettingId::XPROFILE_CRUX_TOP_MEDIAID3, + UserSettingId::XPROFILE_GAMERCARD_AVATAR_INFO_1, + UserSettingId::XPROFILE_GAMERCARD_AVATAR_INFO_2, + UserSettingId::XPROFILE_GAMERCARD_PARTY_INFO, + UserSettingId::XPROFILE_TITLE_SPECIFIC1, + UserSettingId::XPROFILE_TITLE_SPECIFIC2, + UserSettingId::XPROFILE_TITLE_SPECIFIC3, + UserSettingId::XPROFILE_CRUX_LAST_CHANGE_TIME, + UserSettingId::XPROFILE_TENURE_NEXT_MILESTONE_DATE, + UserSettingId::XPROFILE_LAST_LIVE_SIGNIN, +}; + +const static std::set title_writable_settings = { + UserSettingId::XPROFILE_TITLE_SPECIFIC1, + UserSettingId::XPROFILE_TITLE_SPECIFIC2, + UserSettingId::XPROFILE_TITLE_SPECIFIC3}; + +enum PREFERRED_COLOR_OPTIONS : uint32_t { + PREFERRED_COLOR_NONE, + PREFERRED_COLOR_BLACK, + PREFERRED_COLOR_WHITE, + PREFERRED_COLOR_YELLOW, + PREFERRED_COLOR_ORANGE, + PREFERRED_COLOR_PINK, + PREFERRED_COLOR_RED, + PREFERRED_COLOR_PURPLE, + PREFERRED_COLOR_BLUE, + PREFERRED_COLOR_GREEN, + PREFERRED_COLOR_BROWN, + PREFERRED_COLOR_SILVER +}; + +using SettingTypes = std::variant>; + +class UserSetting { + public: + // Ctor for writing from host + UserSetting(UserSettingId setting_id, SettingTypes setting_data); + // Ctor for writing to GPD + UserSetting(const X_USER_PROFILE_SETTING* profile_setting); + // Ctor for reading from GPD + UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting, + std::span extended_data); + + uint32_t get_setting_id() const { return static_cast(setting_id_); } + + std::vector serialize_to_gpd() const; + + const X_USER_DATA* get_base_data(); + + std::span get_extended_data() const { + return {extended_data_.data(), extended_data_.size()}; + } + + X_USER_DATA_TYPE get_setting_type() const { return setting_type_; } + + static X_USER_DATA_TYPE get_setting_type(uint32_t setting_id) { + return static_cast(setting_id >> 28); + } + + static uint16_t get_setting_max_size(uint32_t setting_id) { + return static_cast(setting_id >> 16) & kMaxSettingSize; + } + static bool is_setting_valid(uint32_t setting_id) { + return std::find(known_settings.cbegin(), known_settings.cend(), + static_cast(setting_id)) != + known_settings.cend(); + } + + bool is_valid_setting_type() const { + return setting_type_ >= X_USER_DATA_TYPE::CONTEXT && + setting_type_ <= X_USER_DATA_TYPE::DATETIME; + } + + bool requires_additional_data() const { + return setting_type_ == X_USER_DATA_TYPE::BINARY || + setting_type_ == X_USER_DATA_TYPE::WSTRING; + } + + static bool requires_additional_data(uint32_t setting_id) { + const auto setting_type = get_setting_type(setting_id); + + return setting_type == X_USER_DATA_TYPE::BINARY || + setting_type == X_USER_DATA_TYPE::WSTRING; + } + + static size_t get_setting_data_size(const X_USER_PROFILE_SETTING* setting) { + if (requires_additional_data(setting->setting_id)) { + return std::min(get_setting_max_size(setting->setting_id), + static_cast(setting->data.data.binary.size)); + } + + return get_setting_max_size(setting->setting_id); + } + + private: + UserSettingId setting_id_; + + X_USER_DATA_TYPE setting_type_; + X_USER_PROFILE_SETTING_SOURCE setting_source_; + X_USER_DATA user_data_; + + uint16_t size_; + uint16_t max_size_; + + std::vector extended_data_; + + static bool is_valid_setting(uint32_t title_id, + const UserSettingId setting_id) { + if (title_id != kDashboardID && title_writable_settings.count(setting_id)) { + return true; + } + + if (title_id == kDashboardID && + !title_writable_settings.count(setting_id)) { + return true; + } + return false; + } + + static bool is_title_specific(uint32_t setting_id) { + return (setting_id & 0x3F00) == 0x3F00; + } + + // bool is_setting_writable() + bool is_title_specific() const { + return is_title_specific(static_cast(setting_id_)); + } +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_USER_PROFILE_H_ diff --git a/src/xenia/kernel/xam/user_tracker.cc b/src/xenia/kernel/xam/user_tracker.cc new file mode 100644 index 000000000..78ab2097c --- /dev/null +++ b/src/xenia/kernel/xam/user_tracker.cc @@ -0,0 +1,700 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/user_profile.h" + +#include +#include + +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/user_settings.h" +#include "xenia/kernel/xam/user_tracker.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" + +DECLARE_int32(user_language); + +namespace xe { +namespace kernel { +namespace xam { +UserTracker::UserTracker() : spa_data_(nullptr) {} +UserTracker::~UserTracker() {} + +bool UserTracker::AddUser(uint64_t xuid) { + if (IsUserTracked(xuid)) { + XELOGW("{}: User is already on tracking list!"); + return false; + } + + tracked_xuids_.insert(xuid); + + if (spa_data_) { + AddTitleToPlayedList(xuid); + } + return true; +} + +bool UserTracker::RemoveUser(uint64_t xuid) { + if (!IsUserTracked(xuid)) { + XELOGW("{}: User is not on tracking list!"); + return false; + } + + tracked_xuids_.erase(xuid); + FlushUserData(xuid); + return true; +} + +bool UserTracker::UnlockAchievement(uint64_t xuid, uint32_t achievement_id) { + if (!IsUserTracked(xuid)) { + XELOGW("{}: User is not on tracking list!"); + return false; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + if (!spa_data_) { + return false; + } + + const auto spa_achievement = spa_data_->GetAchievement(achievement_id); + if (!spa_achievement) { + return false; + } + + // Update data in profile gpd. + auto title_info = user->dashboard_gpd_.GetTitleInfo(spa_data_->title_id()); + if (!title_info) { + return false; + } + + // Update title gpd + auto title_gpd = &user->games_gpd_[spa_data_->title_id()]; + // Achievement is unlocked, so we need to add achievement icon + title_gpd->AddImage(spa_achievement->image_id, + spa_data_->GetIcon(spa_achievement->image_id)); + + auto gpd_achievement = title_gpd->GetAchievementEntry(spa_achievement->id); + if (!gpd_achievement) { + return false; + } + + title_info->achievements_unlocked++; + title_info->gamerscore_earned += spa_achievement->gamerscore; + + const std::string achievement_name = spa_data_->GetStringTableEntry( + spa_data_->default_language(), spa_achievement->label_id); + + XELOGI("Player: {} Unlocked Achievement: {}", user->name(), + achievement_name.c_str()); + + gpd_achievement->flags = gpd_achievement->flags | + static_cast(AchievementFlags::kAchieved); + gpd_achievement->unlock_time = Clock::QueryGuestSystemTime(); + + UpdateSettingValue(xuid, kDashboardID, UserSettingId::XPROFILE_GAMERCARD_CRED, + gpd_achievement->gamerscore); + UpdateSettingValue(xuid, kDashboardID, + UserSettingId::XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED, 1); + UpdateSettingValue(xuid, spa_data_->title_id(), + UserSettingId::XPROFILE_GAMERCARD_TITLE_CRED_EARNED, + gpd_achievement->gamerscore); + UpdateSettingValue( + xuid, spa_data_->title_id(), + UserSettingId::XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED, 1); + + FlushUserData(xuid); + return true; +} + +void UserTracker::FlushGpd(const uint64_t xuid, const uint32_t id) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + GpdInfo* gpd = GetGpd(user, id); + if (!gpd) { + return; + } + + if (!gpd->RequiresFlush()) { + return; + } + + std::vector data = gpd->Serialize(); + + vfs::File* file = nullptr; + vfs::FileAction action; + + const auto mounted_path = fmt::format("{:016X}:\\{:08X}.gpd", xuid, id); + + // If entry exist then just update it otherwise create a new file + vfs::Entry* entry = kernel_state()->file_system()->ResolvePath(mounted_path); + + if (entry) { + const auto result = kernel_state()->file_system()->OpenFile( + nullptr, mounted_path, vfs::FileDisposition::kOpen, + vfs::FileAccess::kGenericAll, false, true, &file, &action); + + if (result != X_STATUS_SUCCESS) { + return; + } + + size_t written_bytes = 0; + file->WriteSync(data.data(), data.size(), 0, &written_bytes); + file->Destroy(); + return; + } + + const auto result = kernel_state()->file_system()->OpenFile( + nullptr, mounted_path, vfs::FileDisposition::kCreate, + vfs::FileAccess::kGenericAll, false, true, &file, &action); + + if (result != X_STATUS_SUCCESS) { + return; + } + + size_t written_bytes = 0; + file->WriteSync(data.data(), data.size(), 0, &written_bytes); + file->Destroy(); +} + +void UserTracker::FlushUserData(const uint64_t xuid) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + FlushGpd(xuid, kDashboardID); + + if (spa_data_) { + FlushGpd(xuid, spa_data_->title_id()); + } +} + +void UserTracker::AddTitleToPlayedList() { + if (!spa_data_) { + return; + } + + for (const uint64_t xuid : tracked_xuids_) { + AddTitleToPlayedList(xuid); + } +} + +void UserTracker::AddTitleToPlayedList(uint64_t xuid) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + if (!spa_data_) { + return; + } + + const uint32_t title_id = spa_data_->title_id(); + auto title_gpd = user->games_gpd_.find(title_id); + if (title_gpd == user->games_gpd_.end()) { + user->games_gpd_.emplace(title_id, GpdInfoTitle(title_id)); + UpdateTitleGpdFile(); + } + + if (!spa_data_->include_in_profile()) { + return; + } + + const uint64_t current_time = Clock::QueryGuestSystemTime(); + + auto title_info = user->dashboard_gpd_.GetTitleInfo(title_id); + if (!title_info) { + user->dashboard_gpd_.AddNewTitle(spa_data_); + UpdateSettingValue(xuid, kDashboardID, + UserSettingId::XPROFILE_GAMERCARD_TITLES_PLAYED, 1); + title_info = user->dashboard_gpd_.GetTitleInfo(title_id); + } + // Normally we only need to update last booted time. Everything else is filled + // during creation time OR SPA UPDATE TIME! + title_info->last_played = current_time; + + UpdateProfileGpd(); +} + +// Privates +bool UserTracker::IsUserTracked(uint64_t xuid) const { + return tracked_xuids_.find(xuid) != tracked_xuids_.cend(); +} + +std::optional UserTracker::GetUserTitleInfo( + uint64_t xuid, uint32_t title_id) const { + if (!IsUserTracked(xuid)) { + XELOGW("{}: User is not on tracking list!"); + return std::nullopt; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return std::nullopt; + } + + const auto title_data = user->dashboard_gpd_.GetTitleInfo(title_id); + if (!title_data) { + return std::nullopt; + } + + auto game_gpd = user->games_gpd_.find(title_id); + if (game_gpd == user->games_gpd_.cend()) { + return std::nullopt; + } + + TitleInfo info; + info.id = title_data->title_id; + info.achievements_count = title_data->achievements_count; + info.unlocked_achievements_count = title_data->achievements_unlocked; + info.gamerscore_amount = title_data->gamerscore_total; + info.title_earned_gamerscore = title_data->gamerscore_earned; + info.title_name = xe::to_utf8(user->dashboard_gpd_.GetTitleName(title_id)); + info.icon = game_gpd->second.GetImage(kXdbfIdTitle); + + if ((title_data->last_played >> 56) != 0) { + info.last_played = chrono::WinSystemClock::to_local( + X_ACHIEVEMENT_UNLOCK_TIME(title_data->last_played).to_time_point()); + } + + return info; +} + +std::vector UserTracker::GetPlayedTitles(uint64_t xuid) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + std::vector played_titles; + + const auto titles_data = user->dashboard_gpd_.GetTitlesInfo(); + for (const auto& title_data : titles_data) { + if (!title_data->include_in_enumerator()) { + continue; + } + + TitleInfo info; + info.id = title_data->title_id; + info.achievements_count = title_data->achievements_count; + info.unlocked_achievements_count = title_data->achievements_unlocked; + info.gamerscore_amount = title_data->gamerscore_total; + info.title_earned_gamerscore = title_data->gamerscore_earned; + info.title_name = + xe::to_utf8(user->dashboard_gpd_.GetTitleName(title_data->title_id)); + + if ((title_data->last_played >> 56) != 0) { + info.last_played = chrono::WinSystemClock::to_local( + X_ACHIEVEMENT_UNLOCK_TIME(title_data->last_played).to_time_point()); + } + + auto game_gpd = user->games_gpd_.find(title_data->title_id); + if (game_gpd != user->games_gpd_.cend()) { + info.icon = game_gpd->second.GetImage(kXdbfIdTitle); + } + + played_titles.push_back(info); + } + + std::sort(played_titles.begin(), played_titles.end(), + [](const TitleInfo& first, const TitleInfo& second) { + return first.last_played > second.last_played; + }); + + return played_titles; +} + +void UserTracker::UpdateSpaInfo(SpaInfo* spa_info) { + spa_data_ = spa_info; + + if (!spa_data_) { + return; + } + + UpdateProfileGpd(); + UpdateTitleGpdFile(); +} + +void UserTracker::UpdateTitleGpdFile() { + for (auto& user_xuid : tracked_xuids_) { + auto user = kernel_state()->xam_state()->GetUserProfile(user_xuid); + if (!user) { + continue; + } + + auto game_gpd = user->games_gpd_.find(spa_data_->title_id()); + if (game_gpd == user->games_gpd_.cend()) { + continue; + } + + auto user_language = spa_data_->GetExistingLanguage( + static_cast(cvars::user_language)); + + // First add achievements because of lowest ID + for (const auto& entry : spa_data_->GetAchievements()) { + AchievementDetails details(entry, spa_data_, user_language); + game_gpd->second.AddAchievement(&details); + } + + // Then add game icon + game_gpd->second.AddImage(kXdbfIdTitle, spa_data_->title_icon()); + + // At the end add title name entry + game_gpd->second.AddString(kXdbfIdTitle, + xe::to_utf16(spa_data_->title_name())); + + // Check if we have icon for every unlocked achievements. + FlushUserData(user_xuid); + } +} + +void UserTracker::UpdateProfileGpd() { + for (auto& user_xuid : tracked_xuids_) { + auto user = kernel_state()->xam_state()->GetUserProfile(user_xuid); + if (!user) { + continue; + } + + auto title_data = user->dashboard_gpd_.GetTitleInfo(spa_data_->title_id()); + if (!title_data) { + continue; + } + + const uint32_t achievements_count = spa_data_->achievement_count(); + // If achievements count doesn't match then obviously gamerscore won't match + // either + if (title_data->achievements_count < achievements_count) { + auto title_updated_data = *title_data; + + title_updated_data.achievements_count = achievements_count; + title_updated_data.gamerscore_total = spa_data_->total_gamerscore(); + user->dashboard_gpd_.UpdateTitleInfo(spa_data_->title_id(), + &title_updated_data); + } + + FlushUserData(user_xuid); + } +} + +std::vector UserTracker::GetUserTitleAchievements( + uint64_t xuid, uint32_t title_id) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + auto game_gpd = user->games_gpd_.find(title_id); + if (game_gpd == user->games_gpd_.cend()) { + return {}; + } + + std::vector achievements; + + for (const uint32_t id : game_gpd->second.GetAchievementsIds()) { + Achievement achievement(game_gpd->second.GetAchievementEntry(id)); + + achievement.achievement_name = game_gpd->second.GetAchievementTitle(id); + achievement.unlocked_description = + game_gpd->second.GetAchievementDescription(id); + achievement.locked_description = + game_gpd->second.GetAchievementUnachievedDescription(id); + + achievements.push_back(std::move(achievement)); + } + + return achievements; +}; + +std::span UserTracker::GetAchievementIcon( + uint64_t xuid, uint32_t title_id, uint32_t achievement_id) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + auto game_gpd = user->games_gpd_.find(title_id); + if (game_gpd == user->games_gpd_.cend()) { + return {}; + } + + const auto entry = game_gpd->second.GetAchievementEntry(achievement_id); + if (entry == nullptr) { + return {}; + } + + return game_gpd->second.GetImage(entry->image_id); +} + +void UserTracker::AddProperty(const uint64_t xuid, const Property* property) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + // Find if property already exits + Property* entry = GetProperty(user->xuid(), property->GetPropertyId()); + if (entry) { + *entry = *property; + return; + } + + user->properties_.push_back(*property); +} + +Property* UserTracker::GetProperty(const uint64_t xuid, const AttributeKey id) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return nullptr; + } + + for (auto& entry : user->properties_) { + if (entry.GetPropertyId().value != id.value) { + continue; + } + + return &entry; + } + + return nullptr; +} + +std::optional UserTracker::GetGpdSetting( + UserProfile* user, uint32_t setting_id) const { + if (!spa_data_) { + // There is no data about current title. Use dashboard info. + auto setting = user->dashboard_gpd_.GetSetting(setting_id); + if (!setting) { + return std::nullopt; + } + + return std::make_optional( + setting, user->dashboard_gpd_.GetSettingData(setting_id)); + } + + auto game_gpd = user->games_gpd_.find(spa_data_->title_id()); + if (game_gpd == user->games_gpd_.cend()) { + return std::nullopt; + } + + auto setting = game_gpd->second.GetSetting(setting_id); + if (!setting) { + // Refer to default values + return std::nullopt; + } + + return std::make_optional( + setting, game_gpd->second.GetSettingData(setting_id)); +} + +std::optional UserTracker::GetDefaultSetting( + UserProfile* user, uint32_t setting_id) const { + const auto type = UserSetting::get_setting_type(setting_id); + + if (type == X_USER_DATA_TYPE::WSTRING) { + return std::make_optional( + static_cast(setting_id), std::u16string()); + } + if (type == X_USER_DATA_TYPE::BINARY) { + return std::make_optional( + static_cast(setting_id), std::vector()); + } + + if (type == X_USER_DATA_TYPE::FLOAT) { + return std::make_optional( + static_cast(setting_id), 0.0f); + } + + if (type == X_USER_DATA_TYPE::DOUBLE) { + return std::make_optional( + static_cast(setting_id), 0.0); + } + + if (type == X_USER_DATA_TYPE::DATETIME || type == X_USER_DATA_TYPE::INT64) { + return std::make_optional( + static_cast(setting_id), static_cast(0)); + } + + return std::make_optional(static_cast(setting_id), + 0); +} + +bool UserTracker::GetUserSetting(uint64_t xuid, uint32_t setting_id, + X_USER_PROFILE_SETTING* setting_ptr, + uint32_t& extended_data_address) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + auto gpd_setting = GetGpdSetting(user, setting_id); + // We have entry in gpd so use it + if (gpd_setting) { + setting_ptr->setting_id = setting_id; + setting_ptr->source = X_USER_PROFILE_SETTING_SOURCE::TITLE; + + const auto base_data = gpd_setting->get_base_data(); + memcpy(&setting_ptr->data, base_data, sizeof(X_USER_DATA)); + // Check how to deal with writing additional data + if (gpd_setting->requires_additional_data()) { + const auto extended_data = gpd_setting->get_extended_data(); + + setting_ptr->data.data.binary.size = + static_cast(extended_data.size_bytes()); + setting_ptr->data.data.binary.ptr = extended_data_address; + + memcpy(kernel_memory()->TranslateVirtual(extended_data_address), + extended_data.data(), extended_data.size_bytes()); + + extended_data_address += + static_cast(extended_data.size_bytes()); + } + + return true; + } + + // Get setting from defaults + auto setting = GetDefaultSetting(user, setting_id); + if (setting) { + setting_ptr->setting_id = setting_id; + setting_ptr->source = X_USER_PROFILE_SETTING_SOURCE::DEFAULT; + + const auto base_data = setting->get_base_data(); + memcpy(&setting_ptr->data, base_data, sizeof(X_USER_DATA)); + // There is no additional data for default values + if (setting->requires_additional_data()) { + setting_ptr->data.data.binary.size = 0; + setting_ptr->data.data.binary.ptr = extended_data_address; + } + } + + return true; +} + +void UserTracker::UpdateContext(uint64_t xuid, uint32_t id, uint32_t value) { + if (!IsUserTracked(xuid)) { + return; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + const auto& context_data = spa_data_->GetContext(id); + if (!context_data) { + return; + } + + user->contexts_[id] = value > context_data->max_value + ? static_cast(context_data->default_value) + : value; +} + +std::optional UserTracker::GetUserContext(uint64_t xuid, + uint32_t id) const { + if (!IsUserTracked(xuid)) { + return std::nullopt; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return std::nullopt; + } + + const auto& context_data = spa_data_->GetContext(id); + if (!context_data) { + return std::nullopt; + } + + if (!user->contexts_.count(id)) { + return std::nullopt; + } + + return user->contexts_[id]; +} + +void UserTracker::UpdateSettingValue(uint64_t xuid, uint32_t title_id, + UserSettingId setting_id, + int32_t difference) { + if (!IsUserTracked(xuid)) { + return; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + GpdInfo* info = GetGpd(user, title_id); + if (!info) { + return; + } + + auto setting = info->GetSetting(static_cast(setting_id)); + + if (!setting) { + UserSetting new_setting(setting_id, difference); + info->UpsertSetting(&new_setting); + return; + } + + const int32_t new_value = setting->base_data.s32 + difference; + UserSetting new_setting(setting_id, new_value); + info->UpsertSetting(&new_setting); +} + +void UserTracker::UpsertSetting(uint64_t xuid, uint32_t title_id, + const UserSetting* setting) { + if (!IsUserTracked(xuid)) { + return; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + // Sometimes games like to ignore providing expicitly title_id, so we need to + // check it. + if (!title_id) { + title_id = spa_data_->title_id(); + } + + GpdInfo* info = GetGpd(user, title_id); + if (!info) { + return; + } + + info->UpsertSetting(setting); + FlushUserData(xuid); +} + +GpdInfo* UserTracker::GetGpd(UserProfile* profile, const uint32_t title_id) { + if (title_id == kDashboardID) { + return &profile->dashboard_gpd_; + } + + if (!profile->games_gpd_.count(title_id)) { + return nullptr; + } + + return &profile->games_gpd_[title_id]; +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/user_tracker.h b/src/xenia/kernel/xam/user_tracker.h new file mode 100644 index 000000000..8d092c963 --- /dev/null +++ b/src/xenia/kernel/xam/user_tracker.h @@ -0,0 +1,113 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_USER_TRACKER_H_ +#define XENIA_KERNEL_XAM_USER_TRACKER_H_ + +#include +#include +#include + +#include "xenia/xbox.h" + +#include "xenia/kernel/xam/user_settings.h" + +namespace xe { +namespace kernel { +namespace xam { + +struct TitleInfo { + std::string title_name; + uint32_t id; + uint32_t unlocked_achievements_count; + uint32_t achievements_count; + uint32_t title_earned_gamerscore; + uint32_t gamerscore_amount; + std::chrono::local_time last_played; + + std::span icon; + + bool WasTitlePlayed() const { + return last_played.time_since_epoch().count() != 0; + } +}; + +class UserTracker { + public: + UserTracker(); + ~UserTracker(); + + // UserTracker specific methods + bool AddUser(uint64_t xuid); + bool RemoveUser(uint64_t xuid); + + // SPA related methods + void UpdateSpaInfo(SpaInfo* spa_info); + + // User related methods + bool UnlockAchievement(uint64_t xuid, uint32_t achievement_id); + + // Context + void UpdateContext(uint64_t xuid, uint32_t id, uint32_t value); + std::optional GetUserContext(uint64_t xuid, uint32_t id) const; + + // Property + void AddProperty(const uint64_t xuid, const Property* property); + Property* GetProperty(const uint64_t xuid, const AttributeKey property_id); + + // Settings + void UpsertSetting(uint64_t xuid, uint32_t title_id, + const UserSetting* setting); + + bool GetUserSetting(uint64_t xuid, uint32_t setting_id, + X_USER_PROFILE_SETTING* setting_ptr, + uint32_t& extended_data_address) const; + + // Titles + void AddTitleToPlayedList(); + std::vector GetPlayedTitles(uint64_t xuid) const; + std::optional GetUserTitleInfo(uint64_t xuid, + uint32_t title_id) const; + + // Achievements + std::vector GetUserTitleAchievements(uint64_t xuid, + uint32_t title_id) const; + std::span GetAchievementIcon(uint64_t xuid, uint32_t title_id, + uint32_t achievement_id) const; + + private: + bool IsUserTracked(uint64_t xuid) const; + + void UpdateSettingValue(uint64_t xuid, uint32_t title_id, + UserSettingId setting_id, int32_t difference); + + std::optional GetGpdSetting(UserProfile* user, + uint32_t setting_id) const; + std::optional GetDefaultSetting(UserProfile* user, + uint32_t setting_id) const; + + GpdInfo* GetGpd(UserProfile* profile, const uint32_t title_id); + + void AddTitleToPlayedList(uint64_t xuid); + void UpdateTitleGpdFile(); + void UpdateProfileGpd(); + + void FlushUserData(const uint64_t xuid); + void FlushGpd(const uint64_t xuid, const uint32_t id); + + SpaInfo* spa_data_; + + std::set tracked_xuids_; +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif \ No newline at end of file diff --git a/src/xenia/kernel/xam/xam_state.cc b/src/xenia/kernel/xam/xam_state.cc index 7f7b2c956..b16512e2d 100644 --- a/src/xenia/kernel/xam/xam_state.cc +++ b/src/xenia/kernel/xam/xam_state.cc @@ -25,7 +25,9 @@ XamState::XamState(Emulator* emulator, KernelState* kernel_state) content_manager_ = std::make_unique(kernel_state, content_root); - profile_manager_ = std::make_unique(kernel_state); + user_tracker_ = std::make_unique(); + profile_manager_ = + std::make_unique(kernel_state, user_tracker_.get()); achievement_manager_ = std::make_unique(); AppManager::RegisterApps(kernel_state, app_manager_.get()); @@ -58,6 +60,21 @@ bool XamState::IsUserSignedIn(uint64_t xuid) const { return GetUserProfile(xuid) != nullptr; } +void XamState::LoadSpaInfo(const SpaInfo* info) { + // Check if we have loaded SpaInfo already. If yes then check currently loaded + // version. + if (spa_info_) { + // Trying to load spa with lower version, for whatever reason. + if (*info <= *spa_info_) { + return; + } + } + + spa_info_ = std::make_unique(*info); + spa_info_->Load(); + user_tracker_->UpdateSpaInfo(spa_info_.get()); +} + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/xam_state.h b/src/xenia/kernel/xam/xam_state.h index 4c0526bc4..f72ef8711 100644 --- a/src/xenia/kernel/xam/xam_state.h +++ b/src/xenia/kernel/xam/xam_state.h @@ -15,6 +15,7 @@ #include "xenia/kernel/xam/app_manager.h" #include "xenia/kernel/xam/content_manager.h" #include "xenia/kernel/xam/profile_manager.h" +#include "xenia/kernel/xam/user_tracker.h" namespace xe { class Emulator; @@ -42,19 +43,28 @@ class XamState { } ProfileManager* profile_manager() const { return profile_manager_.get(); } + UserTracker* user_tracker() const { return user_tracker_.get(); } + SpaInfo* spa_info() const { return spa_info_.get(); } + UserProfile* GetUserProfile(uint32_t user_index) const; UserProfile* GetUserProfile(uint64_t xuid) const; bool IsUserSignedIn(uint32_t user_index) const; bool IsUserSignedIn(uint64_t xuid) const; + // + void LoadSpaInfo(const SpaInfo* info); + private: KernelState* kernel_state_; std::unique_ptr app_manager_; std::unique_ptr content_manager_; + std::unique_ptr user_tracker_; std::unique_ptr achievement_manager_; std::unique_ptr profile_manager_; + + std::unique_ptr spa_info_; }; } // namespace xam diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index 1791e5adc..21649efdc 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -17,6 +17,7 @@ #include "xenia/kernel/kernel_flags.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/user_tracker.h" #include "xenia/kernel/xam/xam_content_device.h" #include "xenia/kernel/xam/xam_private.h" #include "xenia/ui/imgui_dialog.h" @@ -520,15 +521,6 @@ struct AchievementInfo { } }; -struct TitleInfo { - std::string title_name; - uint32_t id; - uint32_t unlocked_achievements_count; - uint32_t achievements_count; - uint32_t title_earned_gamerscore; - uint64_t last_played; // Convert from guest to some tm? -}; - class GameAchievementsDialog final : public XamDialog { public: GameAchievementsDialog(ui::ImGuiDrawer* imgui_drawer, @@ -552,21 +544,16 @@ class GameAchievementsDialog final : public XamDialog { ->achievement_manager() ->GetTitleAchievements(profile_->xuid(), title_info_.id); - const auto title_gpd = kernel_state()->title_xdbf(); - - if (!title_achievements) { + if (title_achievements.empty()) { return false; } - for (const auto& entry : *title_achievements) { + for (const auto& entry : title_achievements) { AchievementInfo info; info.id = entry.achievement_id; - info.name = - xe::load_and_swap(entry.achievement_name.c_str()); - info.desc = - xe::load_and_swap(entry.unlocked_description.c_str()); - info.unachieved = - xe::load_and_swap(entry.locked_description.c_str()); + info.name = entry.achievement_name; + info.desc = entry.unlocked_description; + info.unachieved = entry.locked_description; info.flags = entry.flags; info.gamerscore = entry.gamerscore; @@ -580,12 +567,16 @@ class GameAchievementsDialog final : public XamDialog { achievements_info_.insert({info.id, info}); - const auto& icon_entry = - title_gpd.GetEntry(util::XdbfSection::kImage, info.image_id); + const auto icon = + kernel_state() + ->xam_state() + ->achievement_manager() + ->GetAchievementIcon(profile_->xuid(), title_info_.id, + entry.achievement_id); - data.insert({info.image_id, - std::make_pair(icon_entry.buffer, - static_cast(icon_entry.size))}); + data.insert( + {info.image_id, + std::make_pair(icon.data(), static_cast(icon.size()))}); } achievements_icons_ = imgui_drawer()->LoadIcons(data); @@ -659,7 +650,7 @@ class GameAchievementsDialog final : public XamDialog { achievement_entry.unlock_time) .c_str()); } else { - ImGui::TextUnformatted(fmt::format("Unlocked: Locally").c_str()); + ImGui::TextUnformatted(fmt::format("Unlocked: Offline").c_str()); } } @@ -754,40 +745,18 @@ class GamesInfoDialog final : public ui::ImGuiDialog { const UserProfile* profile) { info_.clear(); - // TODO(Gliniak): This code should be adjusted for GPD support. Instead of - // using whole profile it should only take vector of gpd entries. Ideally - // remapped to another struct. - if (kernel_state()->emulator()->is_title_open()) { - const auto xdbf = kernel_state()->title_xdbf(); + xe::ui::IconsData data; - if (!xdbf.is_valid()) { - return; + info_ = kernel_state()->xam_state()->user_tracker()->GetPlayedTitles( + profile->xuid()); + for (const auto& title_info : info_) { + if (!title_info.icon.empty()) { + data[title_info.id] = {title_info.icon.data(), + static_cast(title_info.icon.size())}; } - - const auto title_summary_info = - kernel_state()->achievement_manager()->GetTitleAchievementsInfo( - profile->xuid(), kernel_state()->title_id()); - - if (!title_summary_info) { - return; - } - - TitleInfo game; - game.id = kernel_state()->title_id(); - game.title_name = xdbf.title(); - game.title_earned_gamerscore = title_summary_info->gamerscore; - game.unlocked_achievements_count = - title_summary_info->unlocked_achievements_count; - game.achievements_count = title_summary_info->achievements_count; - game.last_played = 0; - - xe::ui::IconsData data; - const auto& image_data = xdbf.icon(); - data[game.id] = {image_data.buffer, (uint32_t)image_data.size}; - - title_icon = imgui_drawer->LoadIcons(data); - info_.insert({game.id, game}); } + + title_icon = imgui_drawer->LoadIcons(data); } void DrawTitleEntry(ImGuiIO& io, const TitleInfo& entry) { @@ -816,15 +785,23 @@ class GamesInfoDialog final : public ui::ImGuiDialog { ImGui::SetCursorPosY(start_position.y + default_image_icon_size.y - ImGui::GetTextLineHeight()); - // TODO(Gliniak): For now I left hardcoded now, but in the future it must be - // changed to include last time of boot. - ImGui::TextUnformatted(fmt::format("Last played: {}", "Now").c_str()); + if (entry.WasTitlePlayed()) { + ImGui::TextUnformatted( + fmt::format("Last played: {:%Y-%m-%d %H:%M}", entry.last_played) + .c_str()); + } else { + ImGui::TextUnformatted("Last played: Unknown"); + } + + const ImVec2 end_draw_position = + ImVec2(ImGui::GetCursorPos().x - start_position.x, + ImGui::GetCursorPos().y - start_position.y); ImGui::SetCursorPos(start_position); - if (ImGui::Selectable("##Selectable", false, - ImGuiSelectableFlags_SpanAllColumns, - ImGui::GetContentRegionAvail())) { + if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(), + false, ImGuiSelectableFlags_SpanAllColumns, + end_draw_position)) { new GameAchievementsDialog(imgui_drawer(), next_window_position, &entry, profile_); } @@ -851,7 +828,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog { if (!info_.empty()) { if (ImGui::BeginTable("", 2, ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) { - for (const auto& [_, entry] : info_) { + for (const auto& entry : info_) { ImGui::TableNextRow(0, default_image_icon_size.y); DrawTitleEntry(io, entry); } @@ -888,7 +865,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog { const UserProfile* profile_; std::map> title_icon; - std::map info_; + std::vector info_; }; static dword_result_t XamShowMessageBoxUi( @@ -1922,20 +1899,20 @@ dword_result_t XamShowAchievementsUI_entry(dword_t user_index, return X_ERROR_NO_SUCH_USER; } - if (!kernel_state()->title_xdbf().is_valid()) { - return X_ERROR_FUNCTION_FAILED; - } + const auto info = + kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo( + user->xuid(), kernel_state()->xam_state()->spa_info()->title_id()); - TitleInfo info = {}; - info.id = kernel_state()->title_id(); - info.title_name = kernel_state()->title_xdbf().title(); + if (!info) { + return X_ERROR_NO_SUCH_USER; + } ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer(); auto close = [](GameAchievementsDialog* dialog) -> void {}; return xeXamDispatchDialogAsync( - new GameAchievementsDialog(imgui_drawer, ImVec2(100.f, 100.f), &info, - user), + new GameAchievementsDialog(imgui_drawer, ImVec2(100.f, 100.f), + &info.value(), user), close); } DECLARE_XAM_EXPORT1(XamShowAchievementsUI, kUserProfiles, kStub); diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 3b311bc38..639f09a1b 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -7,14 +7,13 @@ ****************************************************************************** */ -#include - #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" @@ -208,21 +207,6 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, be* buffer_size_ptr, uint8_t* buffer, XAM_OVERLAPPED* overlapped) { - if (!xuid_count) { - assert_null(xuids); - } else { - assert_true(xuid_count == 1); - assert_not_null(xuids); - // TODO(gibbed): allow proper lookup of arbitrary XUIDs - // TODO(gibbed): we assert here, but in case a title passes xuid_count > 1 - // until it's implemented for release builds... - xuid_count = 1; - if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) { - const auto& user_profile = - kernel_state()->xam_state()->GetUserProfile(user_index); - assert_true(static_cast(xuids[0]) == user_profile->xuid()); - } - } assert_zero(unk); // probably flags // must have at least 1 to 32 settings @@ -308,13 +292,12 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, bool any_missing = false; for (uint32_t i = 0; i < setting_count; ++i) { auto setting_id = static_cast(setting_ids[i]); - auto setting = user_profile->GetSetting(setting_id); - if (!setting) { - any_missing = true; + if (!UserSetting::is_setting_valid(setting_id)) { XELOGE( "xeXamUserReadProfileSettingsEx requested unimplemented setting " "{:08X}", setting_id); + any_missing = true; } } if (any_missing) { @@ -334,29 +317,24 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, out_header->settings_ptr = kernel_state()->memory()->HostToGuestVirtual(out_setting); - DataByteStream out_stream( - kernel_state()->memory()->HostToGuestVirtual(buffer), buffer, buffer_size, - needed_header_size); + uint32_t additional_data_buffer_ptr = + out_header->settings_ptr + + (setting_count * sizeof(X_USER_PROFILE_SETTING)); + for (uint32_t n = 0; n < setting_count; ++n) { uint32_t setting_id = setting_ids[n]; - auto setting = user_profile->GetSetting(setting_id); - std::memset(out_setting, 0, sizeof(X_USER_PROFILE_SETTING)); - out_setting->from = - !setting ? 0 : static_cast(setting->GetSettingSource()); + auto setting = kernel_state()->xam_state()->user_tracker()->GetUserSetting( + user_profile->xuid(), setting_id, out_setting, + additional_data_buffer_ptr); + if (xuids) { out_setting->xuid = user_profile->xuid(); } else { out_setting->xuid = -1; out_setting->user_index = user_index; } - out_setting->setting_id = setting_id; - if (setting) { - out_setting->data.type = static_cast( - setting->GetSettingHeader()->setting_type.value); - setting->GetSettingData()->Append(&out_setting->data, &out_stream); - } ++out_setting; } @@ -412,53 +390,14 @@ dword_result_t XamUserWriteProfileSettings_entry( } for (uint32_t n = 0; n < setting_count; ++n) { - const X_USER_PROFILE_SETTING& setting = settings[n]; + const UserSetting setting = UserSetting(&settings[n]); - auto setting_type = static_cast(setting.data.type); - if (setting_type == X_USER_DATA_TYPE::UNSET) { + if (!setting.is_valid_setting_type()) { continue; } - XELOGD( - "XamUserWriteProfileSettings: setting index [{}]:" - " from={} setting_id={:08X} data.type={}", - n, (uint32_t)setting.from, (uint32_t)setting.setting_id, - static_cast(setting.data.type)); - - switch (setting_type) { - case X_USER_DATA_TYPE::CONTENT: - case X_USER_DATA_TYPE::BINARY: { - uint8_t* binary_ptr = - kernel_state()->memory()->TranslateVirtual(setting.data.binary.ptr); - - size_t binary_size = setting.data.binary.size; - std::vector bytes; - if (setting.data.binary.ptr) { - // Copy provided data - bytes.resize(binary_size); - std::memcpy(bytes.data(), binary_ptr, binary_size); - } else { - // Data pointer was NULL, so just fill with zeroes - bytes.resize(binary_size, 0); - } - - auto user_setting = - std::make_unique(setting.setting_id, bytes); - - user_setting->SetNewSettingSource(X_USER_PROFILE_SETTING_SOURCE::TITLE); - user_profile->AddSetting(std::move(user_setting)); - } break; - case X_USER_DATA_TYPE::WSTRING: - case X_USER_DATA_TYPE::DOUBLE: - case X_USER_DATA_TYPE::FLOAT: - case X_USER_DATA_TYPE::INT32: - case X_USER_DATA_TYPE::INT64: - case X_USER_DATA_TYPE::DATETIME: - default: { - XELOGE("XamUserWriteProfileSettings: Unimplemented data type {}", - static_cast(setting_type)); - } break; - }; + kernel_state()->xam_state()->user_tracker()->UpsertSetting( + user_profile->xuid(), title_id, &setting); } if (overlapped) { @@ -630,7 +569,6 @@ dword_result_t XamUserCreateAchievementEnumerator_entry( requester_xuid = xuid; } - const util::XdbfGameData db = kernel_state()->title_xdbf(); uint32_t title_id_ = title_id ? static_cast(title_id) : kernel_state()->title_id(); @@ -638,18 +576,12 @@ dword_result_t XamUserCreateAchievementEnumerator_entry( kernel_state()->achievement_manager()->GetTitleAchievements( requester_xuid, title_id_); - if (user_title_achievements) { - for (const auto& entry : *user_title_achievements) { - auto item = XAchievementEnumerator::AchievementDetails{ - entry.achievement_id, - xe::load_and_swap(entry.achievement_name.c_str()), - xe::load_and_swap(entry.unlocked_description.c_str()), - xe::load_and_swap(entry.locked_description.c_str()), - entry.image_id, - entry.gamerscore, - entry.unlock_time.high_part, - entry.unlock_time.low_part, - entry.flags}; + if (!user_title_achievements.empty()) { + for (const auto& entry : user_title_achievements) { + 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, entry.unlock_time, entry.flags); e->AppendItem(item); } diff --git a/src/xenia/kernel/xam/xdbf/gpd_info.cc b/src/xenia/kernel/xam/xdbf/gpd_info.cc new file mode 100644 index 000000000..e61da3705 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info.cc @@ -0,0 +1,312 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/xdbf/gpd_info.h" +#include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/user_settings.h" + +#include +#include + +namespace xe { +namespace kernel { +namespace xam { + +GpdInfo::GpdInfo() : XdbfFile(), title_id_(-1) {} +GpdInfo::GpdInfo(const uint32_t title_id) : XdbfFile(), title_id_(title_id) { + header_.entry_count = 1 * base_entry_count; + header_.free_count = 1 * base_entry_count; + header_.free_used = 1; + + // Add free entry at the end + XdbfFileLoc loc; + loc.size = 0xFFFFFFFF; + loc.offset = 0; + + free_entries_.push_back(loc); +} + +GpdInfo::GpdInfo(const uint32_t title_id, const std::vector buffer) + : XdbfFile({buffer.data(), buffer.size()}), title_id_(title_id) {} + +std::span GpdInfo::GetImage(uint32_t id) const { + const Entry* entry = GetEntry(static_cast(GpdSection::kImage), id); + + if (!entry) { + return {}; + } + + return {entry->data.data(), entry->data.size()}; +} + +void GpdInfo::AddImage(uint32_t id, std::span image_data) { + Entry* entry = GetEntry(static_cast(GpdSection::kImage), id); + + if (entry || !image_data.size()) { + return; + } + + Entry new_entry(id, static_cast(GpdSection::kImage), + static_cast(image_data.size())); + + memcpy(new_entry.data.data(), image_data.data(), image_data.size()); + + UpsertEntry(&new_entry); +} + +X_XDBF_GPD_SETTING_HEADER* GpdInfo::GetSetting(uint32_t id) { + Entry* entry = GetEntry(static_cast(GpdSection::kSetting), id); + + if (!entry) { + return nullptr; + } + + return reinterpret_cast(entry->data.data()); +} + +std::span GpdInfo::GetSettingData(uint32_t id) { + auto* entry = GetSetting(id); + + if (!entry) { + return {}; + } + + if (entry->setting_type != X_USER_DATA_TYPE::BINARY && + entry->setting_type != X_USER_DATA_TYPE::WSTRING) { + return {}; + } + + const uint32_t size = entry->base_data.size; + const uint8_t* data_ptr = reinterpret_cast(entry + 1); + return {data_ptr, size}; +} + +void GpdInfo::UpsertSetting(const UserSetting* setting_data) { + const auto serialized_data = setting_data->serialize_to_gpd(); + + Entry new_entry(setting_data->get_setting_id(), + static_cast(GpdSection::kSetting), + static_cast(serialized_data.size())); + + memcpy(new_entry.data.data(), serialized_data.data(), serialized_data.size()); + + UpsertEntry(&new_entry); +} + +std::u16string GpdInfo::GetString(uint32_t id) const { + const Entry* entry = GetEntry(static_cast(GpdSection::kString), id); + if (!entry) { + return {}; + } + + return string_util::read_u16string_and_swap( + reinterpret_cast(entry->data.data())); +} + +void GpdInfo::AddString(uint32_t id, std::u16string string_data) { + Entry* entry = GetEntry(static_cast(GpdSection::kString), id); + + if (entry != nullptr) { + return; + } + + const uint32_t entry_size = + static_cast(string_util::size_in_bytes(string_data)); + + Entry new_entry(id, static_cast(GpdSection::kString), entry_size); + + string_util::copy_and_swap_truncating( + reinterpret_cast(new_entry.data.data()), string_data, + string_data.length() + 1); + + UpsertEntry(&new_entry); +} + +std::vector GpdInfo::Serialize() const { + // Resize to proper size. + const uint32_t entries_table_size = sizeof(XdbfEntry) * header_.entry_count; + const uint32_t free_table_size = sizeof(XdbfFileLoc) * header_.free_count; + + const uint32_t gpd_size = sizeof(XdbfHeader) + entries_table_size + + free_table_size + CalculateEntriesSize(); + + std::vector data(gpd_size); + + // Header part + uint8_t* write_ptr = data.data(); + // Write header + memcpy(write_ptr, &header_, sizeof(XdbfHeader)); + write_ptr += sizeof(XdbfHeader); + + // Entries in XDBF are sorted by section lowest-to-highest + std::vector entries = GetSortedEntries(); + + for (const auto& entry : entries) { + memcpy(write_ptr, &entry->info, sizeof(XdbfEntry)); + write_ptr += sizeof(XdbfEntry); + } + + const auto empty_entries_count = header_.entry_count - entries.size(); + // Set remaining bytes to 0 + write_ptr = + std::fill_n(write_ptr, empty_entries_count * sizeof(XdbfEntry), 0); + + // Free header part + for (const auto& entry : free_entries_) { + memcpy(write_ptr, &entry, sizeof(XdbfFileLoc)); + write_ptr += sizeof(XdbfFileLoc); + } + + const auto empty_free_entries_count = + header_.free_count - free_entries_.size(); + write_ptr = + std::fill_n(write_ptr, empty_free_entries_count * sizeof(XdbfFileLoc), 0); + + // Entries data + for (const auto& entry : entries) { + if (!entry->info.size) { + continue; + } + + memcpy(write_ptr + entry->info.offset, entry->data.data(), + entry->data.size()); + } + return data; +} + +bool GpdInfo::IsSyncEntry(const Entry* const entry) { + return entry->info.id == 0x100000000 || entry->info.id == 0x200000000; +} + +bool GpdInfo::IsEntryOfSection(const Entry* const entry, + const GpdSection section) { + return entry->info.section == static_cast(section); +} + +void GpdInfo::UpsertEntry(Entry* updated_entry) { + auto entry = GetEntry(updated_entry->info.section, updated_entry->info.id); + + requires_flush_ = true; + + if (entry) { + DeleteEntry(entry); + } + InsertEntry(updated_entry); +} + +uint32_t GpdInfo::FindFreeLocation(const uint32_t entry_size) { + assert_false(free_entries_.empty()); + + uint32_t offset = free_entries_.back().offset; + + auto itr = std::find_if( + free_entries_.begin(), free_entries_.end(), + [entry_size](XdbfFileLoc entry) { return entry.size == entry_size; }); + + // We have exact match, so just get offset and remove entry + if (itr != free_entries_.cend()) { + offset = itr->offset; + header_.free_used--; + free_entries_.erase(itr); + return offset; + } + + // Check for any entry that matches size. + itr = std::find_if( + free_entries_.begin(), free_entries_.end(), + [entry_size](XdbfFileLoc entry) { return entry.size > entry_size; }); + + // There is an requirement that there is always at least one entry, so no need + // to check for valid entry. + offset = itr->offset; + itr->offset += entry_size; + itr->size -= entry_size; + + return offset; +} + +void GpdInfo::InsertEntry(Entry* entry) { + ResizeEntryTable(); + + entry->info.offset = FindFreeLocation(entry->info.size); + + entries_.push_back(*entry); + + header_.entry_used++; +} + +void GpdInfo::DeleteEntry(const Entry* entry) { + // Don't really remove entry. Just remove entry in the entry table. + MarkSpaceAsFree(entry->info.offset, entry->info.size); + + auto itr = + std::find_if(entries_.begin(), entries_.end(), [entry](Entry first) { + return entry->info.section == first.info.section && + first.info.id == entry->info.id; + }); + + if (itr != entries_.end()) { + entries_.erase(itr); + } + header_.entry_used--; +} + +std::vector GpdInfo::GetSortedEntries() const { + std::vector sorted_entries; + + for (auto& entry : entries_) { + sorted_entries.push_back(&entry); + } + + std::sort(sorted_entries.begin(), sorted_entries.end(), + [](const Entry* first, const Entry* second) { + if (first->info.section == second->info.section) { + return first->info.id < second->info.id; + } + return first->info.section < second->info.section; + }); + + return sorted_entries; +} + +void GpdInfo::ResizeEntryTable() { + // There is no need to recalculate offsets as they're in relation to end of + // this entries count. + if (header_.entry_used >= header_.entry_count) { + header_.entry_count = + xe::round_up(header_.entry_count + 1, base_entry_count, true); + } + + if (header_.free_used >= header_.free_count) { + header_.free_used = + xe::round_up(header_.free_used + 1, base_entry_count, true); + } +} + +void GpdInfo::ReallocateEntry(Entry* entry, uint32_t required_size) { + MarkSpaceAsFree(entry->info.offset, entry->info.size); + + // Now find new location for out entry + entry->info.size = required_size; + entry->info.offset = FindFreeLocation(required_size); +} + +void GpdInfo::MarkSpaceAsFree(uint32_t offset, uint32_t size) { + XdbfFileLoc loc; + loc.size = size; + loc.offset = offset; + + ResizeEntryTable(); + free_entries_.emplace(free_entries_.begin(), loc); + header_.free_used++; +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/gpd_info.h b/src/xenia/kernel/xam/xdbf/gpd_info.h new file mode 100644 index 000000000..c08cc1ba3 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info.h @@ -0,0 +1,157 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XDBF_GPD_INFO_H_ +#define XENIA_KERNEL_XAM_XDBF_GPD_INFO_H_ + +#include "xenia/kernel/util/xuserdata.h" +#include "xenia/kernel/xam/xdbf/xdbf_io.h" + +#include +#include +#include + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +class UserSetting; + +enum class GpdSection : uint16_t { + kAchievement = 0x1, // TitleGpd exclusive + kImage = 0x2, + kSetting = 0x3, + kTitle = 0x4, // Dashboard Gpd exclusive + kString = 0x5, + kProtectedAchievement = 0x6, // GFWL only +}; + +struct X_XDBF_AVATARAWARDS_COUNTER { + uint8_t earned; + uint8_t possible; +}; +static_assert_size(X_XDBF_AVATARAWARDS_COUNTER, 2); + +#pragma pack(push, 1) +struct X_XDBF_GPD_ACHIEVEMENT { + xe::be magic; + xe::be id; + xe::be image_id; + xe::be gamerscore; + xe::be flags; + xe::be unlock_time; + // wchar_t* title; + // wchar_t* description; + // wchar_t* unlocked_description; +}; +static_assert_size(X_XDBF_GPD_ACHIEVEMENT, 0x1C); + +struct X_XDBF_GPD_TITLE_PLAYED { + xe::be title_id; + xe::be achievements_count; + xe::be achievements_unlocked; + xe::be gamerscore_total; + xe::be gamerscore_earned; + xe::be online_achievement_count; + + X_XDBF_AVATARAWARDS_COUNTER all_avatar_awards; + X_XDBF_AVATARAWARDS_COUNTER male_avatar_awards; + X_XDBF_AVATARAWARDS_COUNTER female_avatar_awards; + xe::be + flags; // 1 - Offline unlocked, must be synced. 2 - Achievement Unlocked. + // Image missing. 0x10 - Avatar unlocked. Avatar missing. + xe::be last_played; + // xe::be title_name[64]; // size seems to be variable inside GPDs. + + bool include_in_enumerator() const { return achievements_count != 0; } +}; +static_assert_size(X_XDBF_GPD_TITLE_PLAYED, 0x28); + +struct X_XDBF_GPD_SETTING_HEADER { + xe::be setting_id; + xe::be unknown_1; + X_USER_DATA_TYPE setting_type; + char unknown[7]; + + union { + // Size is used only for types: WSTRING, BINARY + xe::be size; + + // Raw values that can be written. They do not need to be serialized. + xe::be s32; + xe::be s64; + xe::be u32; + xe::be f64; + xe::be f32; + } base_data; + + bool RequiresBuffer() const { + return setting_type == X_USER_DATA_TYPE::BINARY || + setting_type == X_USER_DATA_TYPE::WSTRING; + } +}; +static_assert_size(X_XDBF_GPD_SETTING_HEADER, 0x18); +#pragma pack(pop) + +class GpdInfo : public XdbfFile { + public: + GpdInfo(); + GpdInfo(const uint32_t title_id); + GpdInfo(const uint32_t title_id, const std::vector buffer); + + bool RequiresFlush() const { return requires_flush_; } + // Normally GPD ALWAYS contains one free entry that indicates EOF + bool IsValid() const { return !free_entries_.empty(); } + // r/w image, setting, string. + std::span GetImage(uint32_t id) const; + void AddImage(uint32_t id, std::span image_data); + + X_XDBF_GPD_SETTING_HEADER* GetSetting(uint32_t id); + std::span GetSettingData(uint32_t id); + + void UpsertSetting(const UserSetting* setting_data); + + std::u16string GetString(uint32_t id) const; + void AddString(uint32_t id, std::u16string string_data); + + std::vector Serialize() const; + + protected: + bool requires_flush_ = false; + + static bool IsSyncEntry(const Entry* const entry); + static bool IsEntryOfSection(const Entry* const entry, + const GpdSection section); + + void UpsertEntry(Entry* entry); + + uint32_t FindFreeLocation(const uint32_t entry_size); + + private: + static constexpr uint32_t base_entry_count = 512; + + uint32_t title_id_ = 0; + + void InsertEntry(Entry* entry); + void DeleteEntry(const Entry* entry); + + std::vector GetSortedEntries() const; + + void ResizeEntryTable(); + void ReallocateEntry(Entry* entry, uint32_t required_size); + void MarkSpaceAsFree(uint32_t offset, uint32_t size); +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_GPD_INFO_H_ \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/gpd_info_profile.cc b/src/xenia/kernel/xam/xdbf/gpd_info_profile.cc new file mode 100644 index 000000000..8beefa177 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_profile.cc @@ -0,0 +1,115 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/xdbf/gpd_info_profile.h" + +#include "xenia/base/string_util.h" + +#include + +namespace xe { +namespace kernel { +namespace xam { + +const std::vector +GpdInfoProfile::GetTitlesInfo() const { + std::vector entries; + + auto titles = entries_ | std::views::filter([](const auto& entry) { + return !IsSyncEntry(&entry); + }) | + std::views::filter([](const auto& entry) { + return IsEntryOfSection(&entry, GpdSection::kTitle); + }); + + for (const auto& title : titles) { + entries.push_back( + reinterpret_cast(title.data.data())); + } + return entries; +}; + +X_XDBF_GPD_TITLE_PLAYED* GpdInfoProfile::GetTitleInfo(const uint32_t title_id) { + auto title = entries_ | std::views::filter([](const auto& entry) { + return !IsSyncEntry(&entry); + }) | + std::views::filter([](const auto& entry) { + return IsEntryOfSection(&entry, GpdSection::kTitle); + }) | + std::views::filter([title_id](const auto& entry) { + return static_cast(entry.info.id) == title_id; + }); + + if (title.empty()) { + return nullptr; + } + + return reinterpret_cast(title.begin()->data.data()); +} + +std::u16string GpdInfoProfile::GetTitleName(const uint32_t title_id) const { + const Entry* entry = + GetEntry(static_cast(GpdSection::kTitle), title_id); + + if (!entry) { + return std::u16string(); + } + + return string_util::read_u16string_and_swap(reinterpret_cast( + entry->data.data() + sizeof(X_XDBF_GPD_TITLE_PLAYED))); +} + +void GpdInfoProfile::AddNewTitle(const SpaInfo* title_data) { + const X_XDBF_GPD_TITLE_PLAYED title_gpd_data = + FillTitlePlayedData(title_data); + + const std::u16string title_name = xe::to_utf16(title_data->title_name()); + + const uint32_t entry_size = + sizeof(X_XDBF_GPD_TITLE_PLAYED) + + static_cast(string_util::size_in_bytes(title_name)); + + Entry entry(title_data->title_id(), static_cast(GpdSection::kTitle), + entry_size); + + memcpy(entry.data.data(), &title_gpd_data, sizeof(X_XDBF_GPD_TITLE_PLAYED)); + + string_util::copy_and_swap_truncating( + reinterpret_cast(entry.data.data() + + sizeof(X_XDBF_GPD_TITLE_PLAYED)), + title_name, title_name.size() + 1); + + UpsertEntry(&entry); +} + +X_XDBF_GPD_TITLE_PLAYED GpdInfoProfile::FillTitlePlayedData( + const SpaInfo* title_data) const { + X_XDBF_GPD_TITLE_PLAYED title_gpd_data = {}; + + title_gpd_data.title_id = title_data->title_id(); + title_gpd_data.achievements_count = title_data->achievement_count(); + title_gpd_data.gamerscore_total = title_data->total_gamerscore(); + + return title_gpd_data; +} + +void GpdInfoProfile::UpdateTitleInfo(const uint32_t title_id, + X_XDBF_GPD_TITLE_PLAYED* title_data) { + auto current_info = GetTitleInfo(title_id); + if (!current_info) { + return; + } + + memcpy(current_info, title_data, sizeof(X_XDBF_GPD_TITLE_PLAYED)); + requires_flush_ = true; +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/gpd_info_profile.h b/src/xenia/kernel/xam/xdbf/gpd_info_profile.h new file mode 100644 index 000000000..0347c466c --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_profile.h @@ -0,0 +1,50 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XDBF_GPD_INFO_PROFILE_H_ +#define XENIA_KERNEL_XAM_XDBF_GPD_INFO_PROFILE_H_ + +#include "xenia/kernel/xam/xdbf/gpd_info.h" +#include "xenia/kernel/xam/xdbf/spa_info.h" + +#include +#include + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +class GpdInfoProfile : public GpdInfo { + public: + GpdInfoProfile() : GpdInfo(0xFFFE07D1) {}; + GpdInfoProfile(const std::vector buffer) + : GpdInfo(0xFFFE07D1, buffer) {}; + + ~GpdInfoProfile() {}; + + void AddNewTitle(const SpaInfo* title_data); + void UpdateTitleInfo(const uint32_t title_id, + X_XDBF_GPD_TITLE_PLAYED* title_data); + + const std::vector GetTitlesInfo() const; + X_XDBF_GPD_TITLE_PLAYED* GetTitleInfo(const uint32_t title_id); + + std::u16string GetTitleName(const uint32_t title_id) const; + + private: + X_XDBF_GPD_TITLE_PLAYED FillTitlePlayedData(const SpaInfo* title_data) const; +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_GPD_INFO_PROFILE_H_ \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/gpd_info_title.cc b/src/xenia/kernel/xam/xdbf/gpd_info_title.cc new file mode 100644 index 000000000..adb12809f --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_title.cc @@ -0,0 +1,152 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/xdbf/gpd_info_title.h" + +namespace xe { +namespace kernel { +namespace xam { + +X_XDBF_GPD_ACHIEVEMENT* GpdInfoTitle::GetAchievementEntry(const uint32_t id) { + Entry* entry = GetEntry(static_cast(GpdSection::kAchievement), id); + + if (!entry) { + return nullptr; + } + + return reinterpret_cast(entry->data.data()); +} + +const char16_t* GpdInfoTitle::GetAchievementTitlePtr(const uint32_t id) { + X_XDBF_GPD_ACHIEVEMENT* achievement_ptr = GetAchievementEntry(id); + if (!achievement_ptr) { + return nullptr; + } + + return reinterpret_cast(++achievement_ptr); +} + +const char16_t* GpdInfoTitle::GetAchievementDescriptionPtr(const uint32_t id) { + // We need to get ptr to first string. These are one after another in memory. + const char16_t* title_ptr = GetAchievementTitlePtr(id); + if (!title_ptr) { + return nullptr; + } + + return reinterpret_cast(title_ptr + + GetAchievementTitle(id).length()); +} + +const char16_t* GpdInfoTitle::GetAchievementUnachievedDescriptionPtr( + const uint32_t id) { + const char16_t* title_ptr = GetAchievementDescriptionPtr(id); + if (!title_ptr) { + return nullptr; + } + + return reinterpret_cast( + title_ptr + GetAchievementDescription(id).length()); +} + +std::u16string GpdInfoTitle::GetAchievementTitle(const uint32_t id) { + auto title_ptr = GetAchievementTitlePtr(id); + + if (!title_ptr) { + return std::u16string(); + } + + return string_util::read_u16string_and_swap(title_ptr); +} + +std::u16string GpdInfoTitle::GetAchievementDescription(const uint32_t id) { + auto description_ptr = GetAchievementDescriptionPtr(id); + + if (!description_ptr) { + return std::u16string(); + } + + return string_util::read_u16string_and_swap(description_ptr); +} + +std::u16string GpdInfoTitle::GetAchievementUnachievedDescription( + const uint32_t id) { + auto description_ptr = GetAchievementUnachievedDescriptionPtr(id); + + if (!description_ptr) { + return std::u16string(); + } + + return string_util::read_u16string_and_swap(description_ptr); +} + +std::vector GpdInfoTitle::GetAchievementsIds() const { + std::vector ids; + + for (const auto& entry : entries_) { + if (entry.info.section != static_cast(GpdSection::kAchievement)) { + continue; + } + ids.push_back(static_cast(entry.info.id)); + } + return ids; +} + +void GpdInfoTitle::AddAchievement(const AchievementDetails* header) { + Entry* entry = + GetEntry(static_cast(GpdSection::kAchievement), header->id); + + if (entry) { + return; + } + + X_XDBF_GPD_ACHIEVEMENT internal_info; + internal_info.magic = sizeof(X_XDBF_GPD_ACHIEVEMENT); + internal_info.id = header->id; + internal_info.image_id = header->image_id; + internal_info.gamerscore = header->gamerscore; + internal_info.flags = header->flags; + internal_info.unlock_time = 0; + + const uint32_t strings_size = + static_cast(string_util::size_in_bytes(header->label) + + string_util::size_in_bytes(header->description) + + string_util::size_in_bytes(header->unachieved)); + + const uint32_t entry_size = sizeof(X_XDBF_GPD_ACHIEVEMENT) + strings_size; + + Entry new_entry(header->id, static_cast(GpdSection::kAchievement), + entry_size); + + uint8_t* write_ptr = new_entry.data.data(); + memcpy(write_ptr, &internal_info, sizeof(X_XDBF_GPD_ACHIEVEMENT)); + + write_ptr += sizeof(X_XDBF_GPD_ACHIEVEMENT); + + string_util::copy_and_swap_truncating(reinterpret_cast(write_ptr), + header->label, + header->label.length() + 1); + + write_ptr += string_util::size_in_bytes(header->label); + + string_util::copy_and_swap_truncating(reinterpret_cast(write_ptr), + header->description, + header->description.length() + 1); + + write_ptr += string_util::size_in_bytes(header->description); + + string_util::copy_and_swap_truncating(reinterpret_cast(write_ptr), + header->unachieved, + header->unachieved.length() + 1); + + UpsertEntry(&new_entry); +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/gpd_info_title.h b/src/xenia/kernel/xam/xdbf/gpd_info_title.h new file mode 100644 index 000000000..c2d80f965 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_title.h @@ -0,0 +1,55 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_ +#define XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_ + +#include "xenia/kernel/xam/achievement_manager.h" +#include "xenia/kernel/xam/xdbf/gpd_info.h" +#include "xenia/kernel/xam/xdbf/xdbf_io.h" + +#include +#include + +#include "xenia/base/memory.h" +#include "xenia/base/string_util.h" +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +class GpdInfoTitle : public GpdInfo { + public: + GpdInfoTitle() : GpdInfo(-1) {}; + GpdInfoTitle(const uint32_t title_id) : GpdInfo(title_id) {}; + GpdInfoTitle(const uint32_t title_id, const std::vector buffer) + : GpdInfo(title_id, buffer) {}; + + ~GpdInfoTitle() {}; + + std::vector GetAchievementsIds() const; + + void AddAchievement(const AchievementDetails* header); + + X_XDBF_GPD_ACHIEVEMENT* GetAchievementEntry(const uint32_t id); + std::u16string GetAchievementTitle(const uint32_t id); + std::u16string GetAchievementDescription(const uint32_t id); + std::u16string GetAchievementUnachievedDescription(const uint32_t id); + + private: + const char16_t* GetAchievementTitlePtr(const uint32_t id); + const char16_t* GetAchievementDescriptionPtr(const uint32_t id); + const char16_t* GetAchievementUnachievedDescriptionPtr(const uint32_t id); +}; + +} // namespace xam +} // namespace kernel +} // namespace xe +#endif // XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_ \ No newline at end of file diff --git a/src/xenia/kernel/xam/xdbf/spa_info.cc b/src/xenia/kernel/xam/xdbf/spa_info.cc new file mode 100644 index 000000000..e535b4111 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/spa_info.cc @@ -0,0 +1,308 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/xdbf/spa_info.h" +#include + +namespace xe { +namespace kernel { +namespace xam { + +SpaInfo::SpaInfo(const std::span buffer) : XdbfFile(buffer) { + // On creation we only need to load basic info. This is to prevent unnecessary + // load if we have updated SPA in TU/DLC. + LoadTitleInformation(); +} + +void SpaInfo::Load() { + LoadLanguageData(); + LoadAchievements(); + LoadProperties(); + LoadContexts(); +} + +bool operator<(const SpaInfo& first, const SpaInfo& second) { + return std::tie(first.title_header_.major, first.title_header_.minor, + first.title_header_.build, first.title_header_.revision) < + std::tie(second.title_header_.major, second.title_header_.minor, + second.title_header_.build, second.title_header_.revision); +} + +bool operator==(const SpaInfo& first, const SpaInfo& second) { + return std::tie(first.title_header_.major, first.title_header_.minor, + first.title_header_.build, first.title_header_.revision) == + std::tie(second.title_header_.major, second.title_header_.minor, + second.title_header_.build, second.title_header_.revision); +} + +bool operator<=(const SpaInfo& first, const SpaInfo& second) { + return first < second || first == second; +} + +void SpaInfo::LoadLanguageData() { + for (uint64_t language = 1; + language < static_cast(XLanguage::kMaxLanguages); language++) { + auto section = + GetEntry(static_cast(SpaSection::kStringTable), language); + if (!section) { + continue; + } + + auto section_header = + reinterpret_cast(section->data.data()); + assert_true(section_header->magic == kXdbfSignatureXstr); + assert_true(section_header->version == 1); + + const uint8_t* ptr = section->data.data() + sizeof(XdbfSectionHeaderEx); + + XdbfLanguageStrings strings; + + for (uint16_t i = 0; i < section_header->count; i++) { + const XdbfStringTableEntry* entry = + reinterpret_cast(ptr); + + std::string string_data = std::string( + reinterpret_cast(ptr + sizeof(XdbfStringTableEntry)), + entry->string_length); + + strings[entry->id] = string_data; + + ptr += entry->string_length + sizeof(XdbfStringTableEntry); + } + + language_strings_[static_cast(language)] = strings; + } +} + +void SpaInfo::LoadAchievements() { + auto section = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXach); + if (!section) { + return; + } + + auto section_header = + reinterpret_cast(section->data.data()); + assert_true(section_header->magic == kXdbfSignatureXach); + assert_true(section_header->version == 1); + + AchievementTableEntry* ptr = reinterpret_cast( + section->data.data() + sizeof(XdbfSectionHeaderEx)); + + for (uint32_t i = 0; i < section_header->count; i++) { + achievements_.push_back(&ptr[i]); + } +} + +void SpaInfo::LoadProperties() { + auto property_table = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXprp); + + if (!property_table) { + return; + } + + auto xprp_head = + reinterpret_cast(property_table->data.data()); + assert_true(xprp_head->magic == kXdbfSignatureXprp); + assert_true(xprp_head->version == 1); + + const uint8_t* ptr = property_table->data.data() + sizeof(XdbfSectionHeader); + const uint16_t properties_count = + xe::byte_swap(*reinterpret_cast(ptr)); + ptr += sizeof(uint16_t); + + for (uint16_t i = 0; i < properties_count; i++) { + auto entry = reinterpret_cast(ptr); + ptr += sizeof(XdbfPropertyTableEntry); + properties_.push_back(entry); + } +} + +void SpaInfo::LoadContexts() { + auto contexts_table = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXctx); + if (!contexts_table) { + return; + } + + auto xcxt_head = + reinterpret_cast(contexts_table->data.data()); + assert_true(xcxt_head->magic == kXdbfSignatureXcxt); + assert_true(xcxt_head->version == 1); + + const uint8_t* ptr = contexts_table->data.data() + sizeof(XdbfSectionHeader); + const uint32_t contexts_count = + xe::byte_swap(*reinterpret_cast(ptr)); + ptr += sizeof(uint32_t); + + for (uint32_t i = 0; i < contexts_count; i++) { + auto entry = reinterpret_cast(ptr); + ptr += sizeof(XdbfContextTableEntry); + contexts_.push_back(entry); + } +} + +const uint8_t* SpaInfo::ReadXLast(uint32_t& compressed_size, + uint32_t& decompressed_size) { + auto xlast_table = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXsrc); + if (!xlast_table) { + return nullptr; + } + + auto xlast_head = + reinterpret_cast(xlast_table->data.data()); + assert_true(xlast_head->magic == kXdbfSignatureXsrc); + assert_true(xlast_head->version == 1); + + const uint8_t* ptr = xlast_table->data.data() + sizeof(XdbfSectionHeader); + + const uint32_t filename_length = + xe::byte_swap(*reinterpret_cast(ptr)); + ptr += sizeof(uint32_t) + filename_length; + + decompressed_size = xe::byte_swap(*reinterpret_cast(ptr)); + ptr += sizeof(uint32_t); + + compressed_size = xe::byte_swap(*reinterpret_cast(ptr)); + ptr += sizeof(uint32_t); + + return ptr; +} + +XLanguage SpaInfo::GetExistingLanguage(XLanguage language_to_check) const { + // A bit of a hack. Check if title in specific language exist. + // If it doesn't then for sure language is not supported. + return title_name(language_to_check).empty() ? default_language() + : language_to_check; +} + +std::span SpaInfo::title_icon() const { + return GetIcon(kXdbfIdTitle); +} + +XLanguage SpaInfo::default_language() const { + auto block = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXstc); + if (!block) { + return XLanguage::kEnglish; + } + + auto xstc = reinterpret_cast(block->data.data()); + return static_cast(static_cast(xstc->default_language)); +} + +bool SpaInfo::is_system_app() const { + return title_header_.title_type == TitleType::kSystem; +} + +bool SpaInfo::is_demo() const { + return title_header_.title_type == TitleType::kDemo; +} + +bool SpaInfo::include_in_profile() const { + if (title_header_.flags & + static_cast(TitleFlags::kAlwaysIncludeInProfile)) { + return true; + } + + if (title_header_.flags & + static_cast(TitleFlags::kNeverIncludeInProfile)) { + return false; + } + + return !is_demo(); +} + +uint32_t SpaInfo::title_id() const { return title_header_.title_id; } + +std::string SpaInfo::title_name() const { + return GetStringTableEntry(default_language(), kXdbfIdTitle); +} + +std::string SpaInfo::title_name(XLanguage language) const { + return GetStringTableEntry(language, kXdbfIdTitle); +} + +// PRIVATE +void SpaInfo::LoadTitleInformation() { + auto section = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXthd); + if (!section) { + return; + } + + auto section_header = + reinterpret_cast(section->data.data()); + assert_true(section_header->magic == kXdbfSignatureXthd); + assert_true(section_header->version == 1); + + TitleHeaderData* ptr = reinterpret_cast( + section->data.data() + sizeof(XdbfSectionHeader)); + + title_header_ = *ptr; +} + +std::string SpaInfo::GetStringTableEntry(XLanguage language, + uint16_t string_id) const { + auto language_table = language_strings_.find(language); + if (language_table == language_strings_.cend()) { + return ""; + } + + auto entry = language_table->second.find(string_id); + if (entry == language_table->second.cend()) { + return ""; + } + + return entry->second; +} + +const AchievementTableEntry* SpaInfo::GetAchievement(uint32_t id) { + for (const auto& entry : achievements_) { + if (entry->id != id) { + continue; + } + return entry; + } + return nullptr; +} + +const XdbfContextTableEntry* SpaInfo::GetContext(uint32_t id) { + for (const auto& entry : contexts_) { + if (entry->id != id) { + continue; + } + return entry; + } + return nullptr; +} + +const XdbfPropertyTableEntry* SpaInfo::GetProperty(uint32_t id) { + for (const auto& entry : properties_) { + if (entry->id != id) { + continue; + } + return entry; + } + return nullptr; +} + +std::span SpaInfo::GetIcon(uint64_t id) const { + auto entry = GetEntry(static_cast(SpaSection::kImage), id); + if (!entry) { + return {}; + } + return {entry->data.data(), entry->data.size()}; +} + +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/xdbf/spa_info.h b/src/xenia/kernel/xam/xdbf/spa_info.h new file mode 100644 index 000000000..f900d0e3f --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/spa_info.h @@ -0,0 +1,219 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_ +#define XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_ + +#include "xenia/kernel/xam/xdbf/xdbf_io.h" + +#include +#include +#include + +#include "xenia/base/memory.h" +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.h +// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.cpp + +enum class SpaSection : uint16_t { + kMetadata = 0x0001, + kImage = 0x0002, + kStringTable = 0x0003, +}; + +enum class TitleType : uint32_t { + kSystem = 0, + kFull = 1, + kDemo = 2, + kDownload = 3, +}; + +enum class TitleFlags { + kAlwaysIncludeInProfile = 1, + kNeverIncludeInProfile = 2, +}; + +#pragma pack(push, 1) +struct TitleHeaderData { + xe::be title_id; + xe::be title_type; + xe::be major; + xe::be minor; + xe::be build; + xe::be revision; + xe::be flags; + xe::be padding_1; + xe::be padding_2; + xe::be padding_3; +}; +static_assert_size(TitleHeaderData, 32); + +struct StatsViewTableEntry { + xe::be id; + xe::be flags; + xe::be shared_index; + xe::be string_id; + xe::be unused; +}; +static_assert_size(StatsViewTableEntry, 0x10); + +struct ViewFieldEntry { + xe::be size; + xe::be property_id; + xe::be flags; + xe::be attribute_id; + xe::be string_id; + xe::be aggregation_type; + xe::be ordinal; + xe::be field_type; + xe::be format_type; + xe::be unused_1; + xe::be unused_2; +}; +static_assert_size(ViewFieldEntry, 0x20); + +struct SharedViewMetaTableEntry { + xe::be column_count; + xe::be row_count; + xe::be unused_1; + xe::be unused_2; +}; +static_assert_size(SharedViewMetaTableEntry, 0xC); + +struct PropertyBag { + std::vector> contexts; + std::vector> properties; +}; + +struct SharedView { + std::vector column_entries; + std::vector row_entries; + PropertyBag property_bag; +}; + +struct ViewTable { + StatsViewTableEntry view_entry; + SharedView shared_view; +}; + +struct AchievementTableEntry { + xe::be id; + xe::be label_id; + xe::be description_id; + xe::be unachieved_id; + xe::be image_id; + xe::be gamerscore; + xe::be unkE; + xe::be flags; + xe::be unk14; + xe::be unk18; + xe::be unk1C; + xe::be unk20; +}; +static_assert_size(AchievementTableEntry, 0x24); +#pragma pack(pop) + +class SpaInfo : public XdbfFile { + public: + SpaInfo(const std::span buffer); + + void Load(); + + const uint8_t* ReadXLast(uint32_t& compressed_size, + uint32_t& decompressed_size); + + // Checks if provided language exist, if not returns default title language. + XLanguage GetExistingLanguage(XLanguage language_to_check) const; + + // The game icon image, if found. + std::span title_icon() const; + + std::span GetIcon(uint64_t id) const; + + // The game's default language. + XLanguage default_language() const; + + bool is_system_app() const; + bool is_demo() const; + bool include_in_profile() const; + + uint32_t title_id() const; + // The game's title in its default language. + std::string title_name() const; + + std::string title_name(XLanguage language) const; + + uint32_t achievement_count() const { + return static_cast(achievements_.size()); + } + + const AchievementTableEntry* GetAchievement(uint32_t id); + + std::vector GetAchievements() const { + return achievements_; + } + + std::vector GetContexts() const { + return contexts_; + } + + std::vector GetProperties() const { + return properties_; + } + + const XdbfContextTableEntry* GetContext(uint32_t id); + const XdbfPropertyTableEntry* GetProperty(uint32_t id); + + uint32_t total_gamerscore() const { + return std::accumulate(achievements_.cbegin(), achievements_.cend(), 0, + [](uint32_t sum, const auto& entry) { + return sum + entry->gamerscore; + }); + } + + friend bool operator<(const SpaInfo& first, const SpaInfo& second); + friend bool operator<=(const SpaInfo& first, const SpaInfo& second); + friend bool operator==(const SpaInfo& first, const SpaInfo& second); + + std::string GetStringTableEntry(XLanguage language, uint16_t string_id) const; + + private: + // Base info. There should be comparator between different SpaInfos and entry + // with newer data should replace old one. Such situation can happen when game + // adds achievements and so on with DLC. + TitleHeaderData title_header_; + + // SPA is Read-Only so it's reasonable to make it readonly. + std::vector achievements_; + std::vector contexts_; + std::vector properties_; + + typedef std::map XdbfLanguageStrings; + + std::map language_strings_; + + void LoadTitleInformation(); + void LoadAchievements(); + + void LoadLanguageData(); + + void LoadContexts(); + void LoadProperties(); +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_ diff --git a/src/xenia/kernel/xam/xdbf/xdbf_io.cc b/src/xenia/kernel/xam/xdbf/xdbf_io.cc new file mode 100644 index 000000000..ff5f8c33a --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/xdbf_io.cc @@ -0,0 +1,100 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Emulator. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/xam/xdbf/xdbf_io.h" + +namespace xe { +namespace kernel { +namespace xam { + +XdbfFile::XdbfFile(const std::span buffer) { + if (buffer.size() <= sizeof(XdbfHeader)) { + return; + } + + const uint8_t* ptr = buffer.data(); + + if (!LoadHeader(reinterpret_cast(ptr))) { + return; + } + + ptr += sizeof(XdbfHeader); + + const XdbfFileLoc* free_ptr = reinterpret_cast( + ptr + (sizeof(XdbfEntry) * header_.entry_count)); + + const uint8_t* data_ptr = reinterpret_cast(free_ptr) + + (sizeof(XdbfFileLoc) * header_.free_count); + + LoadEntries(reinterpret_cast(ptr), data_ptr); + LoadFreeEntries(free_ptr); +} + +bool XdbfFile::LoadHeader(const XdbfHeader* header) { + if (!header || header->magic != kXdbfSignatureXdbf) { + return false; + } + + memcpy(&header_, header, sizeof(XdbfHeader)); + return true; +} + +uint32_t XdbfFile::CalculateEntriesSize() const { + // XDBF always contains at least 1 free entry that marks EOF. That's why we + // can use it to get size of data in file. + return free_entries_.back().offset; +} + +void XdbfFile::LoadEntries(const XdbfEntry* table_of_content, + const uint8_t* data_ptr) { + if (!table_of_content || !data_ptr) { + return; + } + + for (uint32_t i = 0; i < header_.entry_used; i++) { + entries_.push_back({table_of_content++, data_ptr}); + } +} + +void XdbfFile::LoadFreeEntries(const XdbfFileLoc* free_entries) { + for (uint32_t i = 0; i < header_.free_used; i++) { + free_entries_.push_back(*free_entries); + free_entries++; + } +} + +Entry* XdbfFile::GetEntry(uint16_t section, uint64_t id) { + for (Entry& entry : entries_) { + if (entry.info.id != id || entry.info.section != section) { + continue; + } + return &entry; + } + return nullptr; +} + +const Entry* const XdbfFile::GetEntry(uint16_t section, uint64_t id) const { + for (const Entry& entry : entries_) { + if (entry.info.id != id || entry.info.section != section) { + continue; + } + return &entry; + } + return nullptr; +} + +uint32_t XdbfFile::CalculateDataStartOffset() const { + const uint32_t entry_size = sizeof(XdbfEntry) * header_.entry_count; + const uint32_t free_size = sizeof(XdbfFileLoc) * header_.free_count; + return sizeof(XdbfHeader) + entry_size + free_size; +} + +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/xdbf/xdbf_io.h b/src/xenia/kernel/xam/xdbf/xdbf_io.h new file mode 100644 index 000000000..92cc8eb4d --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/xdbf_io.h @@ -0,0 +1,192 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Emulator. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_XAM_XDBF_XDBF_IO_H_ +#define XENIA_KERNEL_XAM_XDBF_XDBF_IO_H_ + +#include +#include +#include + +#include "xenia/base/memory.h" +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.h +// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Tools/XEX/SPA.cpp + +constexpr fourcc_t kXdbfSignatureXdbf = make_fourcc("XDBF"); +constexpr fourcc_t kXdbfSignatureXstc = make_fourcc("XSTC"); +constexpr fourcc_t kXdbfSignatureXstr = make_fourcc("XSTR"); +constexpr fourcc_t kXdbfSignatureXach = make_fourcc("XACH"); +constexpr fourcc_t kXdbfSignatureXprp = make_fourcc("XPRP"); +constexpr fourcc_t kXdbfSignatureXcxt = make_fourcc("XCXT"); +constexpr fourcc_t kXdbfSignatureXvc2 = make_fourcc("XVC2"); +constexpr fourcc_t kXdbfSignatureXmat = make_fourcc("XMAT"); +constexpr fourcc_t kXdbfSignatureXsrc = make_fourcc("XSRC"); +constexpr fourcc_t kXdbfSignatureXthd = make_fourcc("XTHD"); + +constexpr uint64_t kXdbfIdTitle = 0x8000; +constexpr uint64_t kXdbfIdXstc = 0x58535443; +constexpr uint64_t kXdbfIdXach = 0x58414348; +constexpr uint64_t kXdbfIdXprp = 0x58505250; +constexpr uint64_t kXdbfIdXctx = 0x58435854; +constexpr uint64_t kXdbfIdXvc2 = 0x58564332; +constexpr uint64_t kXdbfIdXmat = 0x584D4154; +constexpr uint64_t kXdbfIdXsrc = 0x58535243; +constexpr uint64_t kXdbfIdXthd = 0x58544844; + +#pragma pack(push, 1) +struct XdbfHeader { + XdbfHeader() { + magic = kXdbfSignatureXdbf; + version = 0x10000; + entry_count = 0; + entry_used = 0; + free_count = 0; + free_used = 0; + } + + xe::be magic; + xe::be version; + xe::be entry_count; + xe::be entry_used; + xe::be free_count; + xe::be free_used; +}; +static_assert_size(XdbfHeader, 24); + +struct XdbfEntry { + xe::be section; + xe::be id; + xe::be offset; + xe::be size; +}; +static_assert_size(XdbfEntry, 18); + +struct XdbfFileLoc { + xe::be offset; + xe::be size; +}; +static_assert_size(XdbfFileLoc, 8); + +struct XdbfXstc { + xe::be magic; + xe::be version; + xe::be size; + xe::be default_language; +}; +static_assert_size(XdbfXstc, 16); + +struct XdbfSectionHeader { + xe::be magic; + xe::be version; + xe::be size; +}; +static_assert_size(XdbfSectionHeader, 12); + +struct XdbfSectionHeaderEx { + xe::be magic; + xe::be version; + xe::be size; + xe::be count; +}; +static_assert_size(XdbfSectionHeaderEx, 14); + +struct XdbfStringTableEntry { + xe::be id; + xe::be string_length; +}; +static_assert_size(XdbfStringTableEntry, 4); + +struct XdbfContextTableEntry { + xe::be id; + xe::be unk1; + xe::be string_id; + xe::be max_value; + xe::be default_value; +}; +static_assert_size(XdbfContextTableEntry, 16); + +struct XdbfPropertyTableEntry { + xe::be id; + xe::be string_id; + xe::be data_size; +}; +static_assert_size(XdbfPropertyTableEntry, 8); +#pragma pack(pop) + +struct XdbfBlock { + const uint8_t* buffer; + size_t size; + + operator bool() const { return buffer != nullptr; } +}; + +struct Entry { + Entry() { + info.id = 0; + info.offset = 0; + info.section = 0; + info.size = 0; + } + + // Offset must be filled externally! + Entry(const uint64_t id, const uint16_t section, const uint32_t size) { + info.id = id; + info.section = section; + info.size = size; + + data.resize(size); + } + + Entry(const XdbfEntry* entry, const uint8_t* entry_data) { + info = *entry; + data.resize(info.size); + memcpy(data.data(), entry_data + info.offset, info.size); + } + + XdbfEntry info; + std::vector data; +}; + +// Wraps an XDBF (XboxDataBaseFormat) in-memory database. +// https://free60project.github.io/wiki/XDBF.html +class XdbfFile { + public: + XdbfFile() {}; + XdbfFile(const std::span buffer); + + const Entry* const GetEntry(uint16_t section, uint64_t id) const; + + protected: + XdbfHeader header_ = {}; + std::vector entries_ = {}; + std::vector free_entries_ = {}; + + // Gets an entry in the given section. + // If the entry is not found the returned block will be nullptr. + Entry* GetEntry(uint16_t section, uint64_t id); + uint32_t CalculateDataStartOffset() const; + uint32_t CalculateEntriesSize() const; + + private: + bool LoadHeader(const XdbfHeader* header); + void LoadEntries(const XdbfEntry* table_of_content, const uint8_t* data_ptr); + void LoadFreeEntries(const XdbfFileLoc* free_entries); +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_XDBF_IO_H_ diff --git a/src/xenia/kernel/xenumerator.h b/src/xenia/kernel/xenumerator.h index 68f2fd9b4..d3b69a227 100644 --- a/src/xenia/kernel/xenumerator.h +++ b/src/xenia/kernel/xenumerator.h @@ -116,20 +116,6 @@ class XStaticEnumerator : public XStaticUntypedEnumerator { class XAchievementEnumerator : public XEnumerator { public: - struct AchievementDetails { - uint32_t id; - std::u16string label; - std::u16string description; - std::u16string unachieved; - uint32_t image_id; - uint32_t gamerscore; - struct { - uint32_t high_part; - uint32_t low_part; - } unlock_time; - uint32_t flags; - }; - XAchievementEnumerator(KernelState* kernel_state, size_t items_per_enumerate, uint32_t flags) : XEnumerator( @@ -139,7 +125,7 @@ class XAchievementEnumerator : public XEnumerator { : 0)), flags_(flags) {} - void AppendItem(AchievementDetails item) { + void AppendItem(xam::AchievementDetails item) { items_.push_back(std::move(item)); } @@ -171,7 +157,7 @@ class XAchievementEnumerator : public XEnumerator { private: uint32_t flags_; - std::vector items_; + std::vector items_; size_t current_item_ = 0; }; diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index 8889a0d8e..69903c6d8 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -249,6 +249,9 @@ std::map> ImGuiDrawer::LoadIcons( stbi_load_from_memory(icon.second.first, icon.second.second, &width, &height, &channels, STBI_rgb_alpha); + if (!image_data) { + continue; + } icons_[icon.first] = (immediate_drawer_->CreateTexture( width, height, ImmediateTextureFilter::kLinear, true, reinterpret_cast(image_data)));