From 58d9f5dae45f2c5d9221d47758f38d82e52235ce Mon Sep 17 00:00:00 2001 From: Gliniak Date: Sun, 15 Dec 2024 13:58:08 +0100 Subject: [PATCH] [Kernel] Added support for writing/reading GPD files This breaks settings in games that are using them and savefiles in games that use settings to store progress --- src/xenia/app/emulator_window.cc | 9 +- src/xenia/base/string_util.h | 23 + src/xenia/emulator.cc | 21 +- src/xenia/emulator.h | 3 + src/xenia/kernel/kernel_state.cc | 34 +- src/xenia/kernel/kernel_state.h | 9 +- src/xenia/kernel/util/game_info_database.cc | 115 +-- src/xenia/kernel/util/game_info_database.h | 9 +- src/xenia/kernel/util/property.cc | 130 --- src/xenia/kernel/util/xdbf_utils.cc | 419 ---------- src/xenia/kernel/util/xdbf_utils.h | 250 ------ src/xenia/kernel/util/xuserdata.h | 234 ------ .../gpd_achievement_backend.cc | 112 ++- .../gpd_achievement_backend.h | 18 +- src/xenia/kernel/xam/achievement_manager.cc | 45 +- src/xenia/kernel/xam/achievement_manager.h | 174 ++-- src/xenia/kernel/xam/apps/xgi_app.cc | 198 +++-- src/xenia/kernel/xam/content_manager.cc | 11 + src/xenia/kernel/xam/profile_manager.cc | 27 +- src/xenia/kernel/xam/profile_manager.h | 41 +- src/xenia/kernel/xam/user_data.cc | 115 +++ src/xenia/kernel/xam/user_data.h | 180 ++++ src/xenia/kernel/xam/user_profile.cc | 312 +++---- src/xenia/kernel/xam/user_profile.h | 207 ++--- src/xenia/kernel/xam/user_property.cc | 75 ++ .../{util/property.h => xam/user_property.h} | 46 +- src/xenia/kernel/xam/user_settings.cc | 127 +++ src/xenia/kernel/xam/user_settings.h | 455 ++++++++++ src/xenia/kernel/xam/user_tracker.cc | 781 ++++++++++++++++++ src/xenia/kernel/xam/user_tracker.h | 123 +++ src/xenia/kernel/xam/xam_state.cc | 19 +- src/xenia/kernel/xam/xam_state.h | 10 + src/xenia/kernel/xam/xam_ui.cc | 312 +++---- src/xenia/kernel/xam/xam_user.cc | 309 ++++--- src/xenia/kernel/xam/xdbf/gpd_info.cc | 309 +++++++ src/xenia/kernel/xam/xdbf/gpd_info.h | 158 ++++ src/xenia/kernel/xam/xdbf/gpd_info_profile.cc | 114 +++ src/xenia/kernel/xam/xdbf/gpd_info_profile.h | 50 ++ src/xenia/kernel/xam/xdbf/gpd_info_title.cc | 196 +++++ src/xenia/kernel/xam/xdbf/gpd_info_title.h | 60 ++ src/xenia/kernel/xam/xdbf/spa_info.cc | 300 +++++++ src/xenia/kernel/xam/xdbf/spa_info.h | 223 +++++ src/xenia/kernel/xam/xdbf/xdbf_io.cc | 100 +++ src/xenia/kernel/xam/xdbf/xdbf_io.h | 192 +++++ src/xenia/kernel/xenumerator.h | 18 +- src/xenia/ui/imgui_drawer.cc | 9 +- src/xenia/ui/imgui_drawer.h | 3 +- src/xenia/vfs/devices/disc_image_entry.cc | 1 + src/xenia/vfs/devices/disc_image_entry.h | 2 + src/xenia/vfs/devices/disc_zarchive_entry.cc | 2 + src/xenia/vfs/devices/disc_zarchive_entry.h | 2 + src/xenia/vfs/devices/host_path_entry.cc | 18 +- src/xenia/vfs/devices/null_entry.cc | 2 + src/xenia/vfs/devices/null_entry.h | 2 + .../vfs/devices/xcontent_container_entry.cc | 2 + .../vfs/devices/xcontent_container_entry.h | 2 + src/xenia/vfs/entry.h | 2 +- src/xenia/xbox.h | 44 + 58 files changed, 4642 insertions(+), 2122 deletions(-) delete mode 100644 src/xenia/kernel/util/property.cc delete mode 100644 src/xenia/kernel/util/xdbf_utils.cc delete mode 100644 src/xenia/kernel/util/xdbf_utils.h delete mode 100644 src/xenia/kernel/util/xuserdata.h create mode 100644 src/xenia/kernel/xam/user_data.cc create mode 100644 src/xenia/kernel/xam/user_data.h create mode 100644 src/xenia/kernel/xam/user_property.cc rename src/xenia/kernel/{util/property.h => xam/user_property.h} (53%) create mode 100644 src/xenia/kernel/xam/user_settings.cc create mode 100644 src/xenia/kernel/xam/user_settings.h create mode 100644 src/xenia/kernel/xam/user_tracker.cc create mode 100644 src/xenia/kernel/xam/user_tracker.h create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info.cc create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info.h create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info_profile.cc create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info_profile.h create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info_title.cc create mode 100644 src/xenia/kernel/xam/xdbf/gpd_info_title.h create mode 100644 src/xenia/kernel/xam/xdbf/spa_info.cc create mode 100644 src/xenia/kernel/xam/xdbf/spa_info.h create mode 100644 src/xenia/kernel/xam/xdbf/xdbf_io.cc create mode 100644 src/xenia/kernel/xam/xdbf/xdbf_io.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 5b8b8a866..5e62a4d50 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1799,10 +1799,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 120839b51..4d5a71dac 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..105d31550 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,24 @@ 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.insert(data.begin(), icon.begin(), icon.end()); return data; } @@ -76,17 +77,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 +94,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 +114,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,31 +133,29 @@ 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; } std::vector GameInfoDatabase::GetMatchmakingAttributes( const uint32_t id) const { + // TODO(Gliniak): Implement when we will fully understand how to read it from + // SPA. std::vector result; - - const auto xdbf_matchmaking_data = xdbf_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 +241,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 +257,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 +273,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 deleted file mode 100644 index 83d32d70e..000000000 --- a/src/xenia/kernel/util/property.cc +++ /dev/null @@ -1,130 +0,0 @@ -/** - ****************************************************************************** - * 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/util/property.h" -#include "xenia/base/logging.h" - -namespace xe { -namespace kernel { - -Property::Property(uint32_t property_id, uint32_t value_size, - uint8_t* value_ptr) { - property_id_.value = property_id; - data_type_ = static_cast(property_id_.type); - - value_size_ = value_size; - value_.resize(value_size); - - if (value_ptr != 0) { - memcpy(value_.data(), value_ptr, value_size); - } else { - XELOGW("{} Ctor: provided value_ptr is nullptr!", __func__); - } -} - -Property::Property(const uint8_t* serialized_data, size_t data_size) { - if (data_size < 8) { - XELOGW("Property::Property lacks information. Skipping!"); - return; - } - - memcpy(&property_id_, serialized_data, sizeof(property_id_)); - data_type_ = static_cast(property_id_.type); - - memcpy(&value_size_, serialized_data + 4, sizeof(value_size_)); - - value_.resize(value_size_); - memcpy(value_.data(), serialized_data + 8, value_size_); -} - -Property::~Property() {}; - -std::vector Property::Serialize() const { - std::vector serialized_property; - serialized_property.resize(sizeof(property_id_) + sizeof(value_size_) + - value_.size()); - - size_t offset = 0; - memcpy(serialized_property.data(), &property_id_, sizeof(property_id_)); - offset += sizeof(property_id_); - - memcpy(serialized_property.data() + offset, &value_size_, - sizeof(value_size_)); - - offset += sizeof(value_size_); - - memcpy(serialized_property.data() + offset, value_.data(), value_.size()); - - return serialized_property; -} - -void Property::Write(Memory* memory, XUSER_PROPERTY* property) const { - property->property_id = property_id_.value; - property->data.type = data_type_; - - switch (data_type_) { - case X_USER_DATA_TYPE::WSTRING: - property->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 pointer must be valid at this point! - memcpy(memory->TranslateVirtual(property->data.binary.ptr), value_.data(), - value_size_); - break; - case X_USER_DATA_TYPE::INT32: - memcpy(reinterpret_cast(&property->data.s32), value_.data(), - value_size_); - break; - case X_USER_DATA_TYPE::INT64: - memcpy(reinterpret_cast(&property->data.s64), value_.data(), - value_size_); - break; - case X_USER_DATA_TYPE::DOUBLE: - memcpy(reinterpret_cast(&property->data.f64), value_.data(), - value_size_); - break; - case X_USER_DATA_TYPE::FLOAT: - memcpy(reinterpret_cast(&property->data.f32), value_.data(), - value_size_); - break; - case X_USER_DATA_TYPE::DATETIME: - memcpy(reinterpret_cast(&property->data.filetime), - value_.data(), value_size_); - break; - default: - break; - } -} - -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::INT32: - return *reinterpret_cast(value_.data()); - case X_USER_DATA_TYPE::INT64: - case X_USER_DATA_TYPE::DATETIME: - return *reinterpret_cast(value_.data()); - case X_USER_DATA_TYPE::DOUBLE: - return *reinterpret_cast(value_.data()); - case X_USER_DATA_TYPE::WSTRING: - return std::u16string(reinterpret_cast(value_.data())); - case X_USER_DATA_TYPE::FLOAT: - return *reinterpret_cast(value_.data()); - default: - break; - } - return value_; -} - -} // namespace kernel -} // namespace xe \ No newline at end of file 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 deleted file mode 100644 index a601c91c5..000000000 --- a/src/xenia/kernel/util/xuserdata.h +++ /dev/null @@ -1,234 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2024 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_KERNEL_UTIL_XUSERDATA_H_ -#define XENIA_KERNEL_UTIL_XUSERDATA_H_ - -#include "xenia/base/byte_stream.h" -#include "xenia/xbox.h" - -namespace xe { -namespace kernel { - -union AttributeKey { - uint32_t value; - struct { - uint32_t id : 14; - uint32_t unk : 2; - uint32_t size : 12; - uint32_t type : 4; - }; -}; - -enum class X_USER_DATA_TYPE : uint8_t { - CONTENT = 0, - INT32 = 1, - INT64 = 2, - DOUBLE = 3, - WSTRING = 4, - FLOAT = 5, - BINARY = 6, - DATETIME = 7, - UNSET = 0xFF, -}; - -struct X_USER_DATA { - X_USER_DATA_TYPE type; - - union { - be s32; - be s64; - be u32; - be f64; - struct { - be size; - be ptr; - } unicode; - be f32; - struct { - be size; - be ptr; - } binary; - be filetime; - }; -}; -static_assert_size(X_USER_DATA, 16); - -class DataByteStream : public ByteStream { - public: - DataByteStream(uint32_t ptr, uint8_t* data, size_t data_length, - size_t offset = 0) - : ByteStream(data, data_length, offset), ptr_(ptr) {} - - uint32_t ptr() const { return static_cast(ptr_ + offset()); } - - private: - uint32_t ptr_; -}; - -class UserData { - public: - UserData() {}; - UserData(X_USER_DATA_TYPE type) { data_.type = type; } - - virtual void Append(X_USER_DATA* data, DataByteStream* stream) { - data->type = data_.type; - } - - virtual std::vector Serialize() const { - return std::vector(); - } - virtual void Deserialize(std::vector) {} - - private: - X_USER_DATA data_ = {}; -}; - -class Int32UserData : public UserData { - public: - Int32UserData(int32_t value) - : UserData(X_USER_DATA_TYPE::INT32), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->s32 = value_; - } - - private: - int32_t value_; -}; - -class Uint32UserData : public UserData { - public: - Uint32UserData(uint32_t value) - : UserData(X_USER_DATA_TYPE::INT32), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->u32 = value_; - } - - private: - uint32_t value_; -}; - -class Int64UserData : public UserData { - public: - Int64UserData(int64_t value) - : UserData(X_USER_DATA_TYPE::INT64), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->s64 = value_; - } - - private: - int64_t value_; -}; - -class FloatUserData : public UserData { - public: - FloatUserData(float value) - : UserData(X_USER_DATA_TYPE::FLOAT), value_(value) {} - - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->f32 = value_; - } - - private: - float value_; -}; - -class DoubleUserData : public UserData { - public: - DoubleUserData(double value) - : UserData(X_USER_DATA_TYPE::DOUBLE), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->f64 = value_; - } - - private: - double value_; -}; - -class UnicodeUserData : public UserData { - public: - UnicodeUserData(const std::u16string& value) - : UserData(X_USER_DATA_TYPE::WSTRING), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - - if (value_.empty()) { - data->unicode.size = 0; - 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(); - auto buffer = - reinterpret_cast(&stream->data()[stream->offset()]); - stream->Advance(size); - copy_and_swap(buffer, (uint16_t*)value_.data(), count); - } - - private: - std::u16string value_; -}; - -class BinaryUserData : public UserData { - public: - BinaryUserData(const std::vector& value) - : UserData(X_USER_DATA_TYPE::BINARY), value_(value) {} - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - - if (value_.empty()) { - data->binary.size = 0; - 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(); - stream->Write(value_.data(), size); - } - - std::vector Serialize() const override { - return std::vector(value_.data(), value_.data() + value_.size()); - } - - void Deserialize(std::vector data) override { value_ = data; } - - private: - std::vector value_; -}; - -class DateTimeUserData : public UserData { - public: - DateTimeUserData(int64_t value) - : UserData(X_USER_DATA_TYPE::DATETIME), value_(value) {} - - void Append(X_USER_DATA* data, DataByteStream* stream) override { - UserData::Append(data, stream); - data->filetime = value_; - } - - private: - int64_t value_; -}; - -} // namespace kernel -} // namespace xe - -#endif 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..cb96a11f7 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 { @@ -40,48 +41,6 @@ enum class AchievementPlatform : uint32_t { kWebGames = 0x400000, }; -enum class AchievementFlags : uint32_t { - kTypeMask = 0x7, - kShowUnachieved = 0x8, - kAchievedOnline = 0x10000, - kAchieved = 0x20000, - kNotAchievable = 0x40000, - kWasNotAchievable = 0x80000, - kPlatformMask = 0x700000, - kColorizable = 0x1000000, // avatar awards only? -}; - -struct X_ACHIEVEMENT_UNLOCK_TIME { - xe::be high_part; - xe::be low_part; - - X_ACHIEVEMENT_UNLOCK_TIME() { - high_part = 0; - low_part = 0; - } - - X_ACHIEVEMENT_UNLOCK_TIME(uint64_t filetime) { - high_part = static_cast(filetime >> 32); - low_part = static_cast(filetime); - } - - X_ACHIEVEMENT_UNLOCK_TIME(std::time_t time) { - const auto file_time = - chrono::WinSystemClock::to_file_time(chrono::WinSystemClock::from_sys( - std::chrono::system_clock::from_time_t(time))); - - high_part = static_cast(file_time >> 32); - low_part = static_cast(file_time); - } - - chrono::WinSystemClock::time_point to_time_point() const { - const uint64_t filetime = - (static_cast(high_part) << 32) | low_part; - - return chrono::WinSystemClock::from_file_time(filetime); - } -}; - struct X_ACHIEVEMENT_DETAILS { xe::be id; xe::be label_ptr; @@ -89,13 +48,60 @@ struct X_ACHIEVEMENT_DETAILS { xe::be unachieved_ptr; xe::be image_id; xe::be gamerscore; - X_ACHIEVEMENT_UNLOCK_TIME unlock_time; + X_FILETIME unlock_time; xe::be flags; static const size_t kStringBufferSize = 464; }; 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_FILETIME 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_FILETIME 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 +109,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,26 +146,23 @@ 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; - X_ACHIEVEMENT_UNLOCK_TIME unlock_time; + uint32_t achievement_id = 0; + uint32_t image_id = 0; + uint32_t gamerscore = 0; + uint32_t flags = 0; + X_FILETIME unlock_time; std::u16string achievement_name; std::u16string unlocked_description; std::u16string locked_description; bool IsUnlocked() const { return (flags & static_cast(AchievementFlags::kAchieved)) || - flags & static_cast(AchievementFlags::kAchievedOnline); + IsUnlockedOnline(); } -}; -struct TitleAchievementsProfileInfo { - uint32_t achievements_count; - uint32_t unlocked_achievements_count; - uint32_t gamerscore; + bool IsUnlockedOnline() const { + return (flags & static_cast(AchievementFlags::kAchievedOnline)); + } }; class AchievementBackendInterface { @@ -161,44 +176,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 63ff1cdf5..c49501db6 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -24,11 +24,38 @@ namespace apps { * https://github.com/NicolasDe/AlienSwarm/blob/master/src/common/xbox/xboxstubs.h */ -struct X_XUSER_ACHIEVEMENT { +struct XGI_XUSER_ACHIEVEMENT { xe::be user_idx; xe::be achievement_id; }; +struct XGI_XUSER_GET_PROPERTY { + xe::be user_index; + xe::be unused; + xe::be xuid; // If xuid is 0 then user_index is used. + xe::be + property_size_ptr; // Normally filled with sizeof(XUSER_PROPERTY), with + // exception of binary and wstring type. + xe::be context_address; + xe::be property_address; +}; + +struct XGI_XUSER_SET_CONTEXT { + xe::be user_index; + xe::be unused; + xe::be xuid; + XUSER_CONTEXT context; +}; + +struct XGI_XUSER_SET_PROPERTY { + xe::be user_index; + xe::be unused; + xe::be xuid; + xe::be property_id; + xe::be data_size; + xe::be data_address; +}; + struct XUSER_STATS_VIEW { xe::be ViewId; xe::be TotalViewRows; @@ -63,63 +90,58 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, auto buffer = memory_->TranslateVirtual(buffer_ptr); switch (message) { case 0x000B0006: { - assert_true(!buffer_length || buffer_length == 24); - // dword r3 user index - // dword (unwritten?) - // qword 0 - // dword r4 context enum - // dword r5 value - uint32_t user_index = xe::load_and_swap(buffer + 0); - uint32_t context_id = xe::load_and_swap(buffer + 16); - uint32_t context_value = xe::load_and_swap(buffer + 20); - XELOGD("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index, - context_id, context_value); + assert_true(!buffer_length || + buffer_length == sizeof(XGI_XUSER_SET_CONTEXT)); + const XGI_XUSER_SET_CONTEXT* xgi_context = + reinterpret_cast(buffer); - 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); + XELOGD("XGIUserSetContext({:08X}, ID: {:08X}, Value: {:08X})", + xgi_context->user_index.get(), + xgi_context->context.context_id.get(), + xgi_context->context.value.get()); - UserProfile* user_profile = - kernel_state_->xam_state()->GetUserProfile(user_index); - if (user_profile) { - user_profile->contexts_[context_id] = context_value; - } + UserProfile* user = nullptr; + if (xgi_context->xuid != 0) { + user = kernel_state_->xam_state()->GetUserProfile(xgi_context->xuid); + } else { + user = + kernel_state_->xam_state()->GetUserProfile(xgi_context->user_index); + } + + if (user) { + kernel_state_->xam_state()->user_tracker()->UpdateContext( + user->xuid(), xgi_context->context.context_id, + xgi_context->context.value); } return X_E_SUCCESS; } case 0x000B0007: { - uint32_t user_index = xe::load_and_swap(buffer + 0); - uint32_t property_id = xe::load_and_swap(buffer + 16); - uint32_t value_size = xe::load_and_swap(buffer + 20); - uint32_t value_ptr = xe::load_and_swap(buffer + 24); - XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", user_index, - property_id, value_size, value_ptr); + const XGI_XUSER_SET_PROPERTY* xgi_property = + reinterpret_cast(buffer); - 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); + XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", + xgi_property->user_index.get(), xgi_property->property_id.get(), + xgi_property->data_size.get(), xgi_property->data_address.get()); - 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); + UserProfile* user = nullptr; + if (xgi_property->xuid != 0) { + user = kernel_state_->xam_state()->GetUserProfile(xgi_property->xuid); + } else { + user = kernel_state_->xam_state()->GetUserProfile( + xgi_property->user_index); } + if (user) { + Property property( + xgi_property->property_id, + UserSetting::get_valid_data_size(xgi_property->property_id, + xgi_property->data_size), + + memory_->TranslateVirtual(xgi_property->data_address)); + + kernel_state_->xam_state()->user_tracker()->AddProperty(user->xuid(), + &property); + } return X_E_SUCCESS; } case 0x000B0008: { @@ -130,7 +152,7 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, achievements_ptr); auto* achievement = - (X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr); + memory_->TranslateVirtual(achievements_ptr); for (uint32_t i = 0; i < achievement_count; i++, achievement++) { kernel_state_->achievement_manager()->EarnAchievement( achievement->user_idx, kernel_state_->title_id(), @@ -219,36 +241,68 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, } case 0x000B003D: { // Used in 5451082A, 5553081E - // XUserGetCachedANID XELOGI("XUserGetANID({:08X}, {:08X}), implemented in netplay", buffer_ptr, buffer_length); return X_E_FAIL; } case 0x000B0041: { - assert_true(!buffer_length || buffer_length == 32); - // 00000000 2789fecc 00000000 00000000 200491e0 00000000 200491f0 20049340 - uint32_t user_index = xe::load_and_swap(buffer + 0); - uint32_t context_ptr = xe::load_and_swap(buffer + 16); - auto context = - context_ptr ? memory_->TranslateVirtual(context_ptr) : nullptr; - uint32_t context_id = - context ? xe::load_and_swap(context + 0) : 0; - XELOGD("XGIUserGetContext({:08X}, {:08X}{:08X}))", user_index, - context_ptr, context_id); - uint32_t value = 0; - if (context) { - 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]; - } - } - xe::store_and_swap(context + 4, value); + assert_true(!buffer_length || + buffer_length == sizeof(XGI_XUSER_GET_PROPERTY)); + const XGI_XUSER_GET_PROPERTY* xgi_property = + reinterpret_cast(buffer); + + UserProfile* user = nullptr; + if (xgi_property->xuid != 0) { + user = kernel_state_->xam_state()->GetUserProfile(xgi_property->xuid); + } else { + user = kernel_state_->xam_state()->GetUserProfile( + xgi_property->user_index); } - return X_E_SUCCESS; + + if (!user) { + XELOGD( + "XGIUserGetProperty - Invalid user provided: Index: {:08X} XUID: " + "{:16X}", + xgi_property->user_index.get(), xgi_property->xuid.get()); + return X_E_NOTFOUND; + } + + // Process context + if (xgi_property->context_address) { + XUSER_CONTEXT* context = memory_->TranslateVirtual( + xgi_property->context_address); + + XELOGD("XGIUserGetProperty - Context requested: {:08X} XUID: {:16X}", + context->context_id.get(), user->xuid()); + + auto context_value = + kernel_state_->xam_state()->user_tracker()->GetUserContext( + user->xuid(), context->context_id); + + if (!context_value) { + return X_E_INVALIDARG; + } + + context->value = context_value.value(); + return X_E_SUCCESS; + } + + if (!xgi_property->property_size_ptr || !xgi_property->property_address) { + return X_E_INVALIDARG; + } + + // Process property + XUSER_PROPERTY* property = memory_->TranslateVirtual( + xgi_property->property_address); + + XELOGD("XGIUserGetProperty - Property requested: {:08X} XUID: {:16X}", + property->property_id.get(), user->xuid()); + + return kernel_state_->xam_state()->user_tracker()->GetProperty( + user->xuid(), + memory_->TranslateVirtual(xgi_property->property_size_ptr), + property); } case 0x000B0071: { XELOGD("XGIUnkB0071({:08X}, {:08X}), unimplemented", buffer_ptr, 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..bdd73ffd3 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) { @@ -440,12 +440,19 @@ uint8_t ProfileManager::GetUserIndexAssignedToProfile( } std::filesystem::path ProfileManager::GetProfileContentPath( - const uint64_t xuid, const uint32_t title_id) const { + const uint64_t xuid, const uint32_t title_id, + const XContentType content_type) const { std::filesystem::path profile_content_path = kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid); if (title_id != -1 && title_id != 0) { profile_content_path = profile_content_path / fmt::format("{:08X}", title_id); + + if (content_type != XContentType::kInvalid) { + profile_content_path = + profile_content_path / + fmt::format("{:08X}", static_cast(content_type)); + } } return profile_content_path; } diff --git a/src/xenia/kernel/xam/profile_manager.h b/src/xenia/kernel/xam/profile_manager.h index 7e085c257..ed5bf3a58 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 { @@ -34,33 +42,6 @@ constexpr uint32_t kDashboardID = 0xFFFE07D1; const static std::string kDashboardStringID = fmt::format("{:08X}", kDashboardID); -enum class XTileType { - kAchievement, - kGameIcon, - kGamerTile, - kGamerTileSmall, - kLocalGamerTile, - kLocalGamerTileSmall, - kBkgnd, - kAwardedGamerTile, - kAwardedGamerTileSmall, - kGamerTileByImageId, - kPersonalGamerTile, - kPersonalGamerTileSmall, - kGamerTileByKey, - kAvatarGamerTile, - kAvatarGamerTileSmall, - kAvatarFullBody -}; - -// TODO: find filenames of other tile types that are stored in profile -static const std::map kTileFileNames = { - {XTileType::kPersonalGamerTile, "tile_64.png"}, - {XTileType::kPersonalGamerTileSmall, "tile_32.png"}, - {XTileType::kAvatarGamerTile, "avtr_64.png"}, - {XTileType::kAvatarGamerTileSmall, "avtr_32.png"}, -}; - class ProfileManager { public: static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output, @@ -75,7 +56,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(); @@ -114,7 +95,8 @@ class ProfileManager { bool IsAnyProfileSignedIn() const { return !logged_profiles_.empty(); } std::filesystem::path GetProfileContentPath( - const uint64_t xuid, const uint32_t title_id = -1) const; + const uint64_t xuid, const uint32_t title_id = -1, + const XContentType content_type = XContentType::kInvalid) const; static bool IsGamertagValid(const std::string gamertag); @@ -142,6 +124,7 @@ class ProfileManager { std::map> logged_profiles_; KernelState* kernel_state_; + UserTracker* user_tracker_; }; } // namespace xam diff --git a/src/xenia/kernel/xam/user_data.cc b/src/xenia/kernel/xam/user_data.cc new file mode 100644 index 000000000..679f713b0 --- /dev/null +++ b/src/xenia/kernel/xam/user_data.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/user_data.h" +#include "xenia/base/string_util.h" +#include "xenia/kernel/util/shim_utils.h" + +namespace xe { +namespace kernel { +namespace xam { + +UserData::UserData() {} +UserData::~UserData() {} + +UserData::UserData(const UserData& user_data) { + data_ = user_data.data_; + extended_data_ = user_data.extended_data_; +} + +UserData::UserData(X_USER_DATA_TYPE data_type, UserDataTypes user_data) { + data_.data = {}; + data_.type = data_type; + + switch (data_type) { + case X_USER_DATA_TYPE::BINARY: + extended_data_ = std::get>(user_data); + data_.data.binary.size = static_cast(extended_data_.size()); + break; + case X_USER_DATA_TYPE::WSTRING: { + std::u16string str = std::get(user_data); + data_.data.unicode.size = + static_cast(string_util::size_in_bytes(str)); + + extended_data_.resize(data_.data.unicode.size); + memcpy(extended_data_.data(), reinterpret_cast(str.data()), + data_.data.unicode.size); + break; + } + case X_USER_DATA_TYPE::INT32: + data_.data.s32 = std::get(user_data); + break; + case X_USER_DATA_TYPE::FLOAT: + data_.data.f32 = std::get(user_data); + break; + case X_USER_DATA_TYPE::CONTEXT: + data_.data.s32 = std::get(user_data); + break; + case X_USER_DATA_TYPE::DOUBLE: + data_.data.f64 = std::get(user_data); + break; + case X_USER_DATA_TYPE::DATETIME: + case X_USER_DATA_TYPE::INT64: + data_.data.s64 = std::get(user_data); + break; + default: + assert_always(); + } +} + +UserData::UserData(const X_USER_DATA_TYPE data_type, + const uint32_t data_max_size, const X_USER_DATA* user_data) { + memcpy(&data_, user_data, sizeof(X_USER_DATA)); + data_.type = data_type; + + if (requires_additional_data()) { + data_.data.binary.size = std::min( + static_cast(data_.data.binary.size), kMaxUserDataSize); + + if (!data_.data.binary.size) { + data_.data.binary.size = data_max_size; + } + + extended_data_.resize(data_.data.binary.size); + memcpy( + extended_data_.data(), + kernel_memory()->TranslateVirtual(user_data->data.binary.ptr), + data_.data.binary.size); + } +} + +UserData::UserData(X_USER_DATA_TYPE data_type, std::span data) { + data_.data = {}; + data_.type = data_type; + + if (!requires_additional_data()) { + memcpy(&data_.data, data.data(), + std::min(static_cast(8), data.size())); + return; + } + + data_.data.binary.size = static_cast(data.size()); + extended_data_.insert(extended_data_.begin(), data.begin(), data.end()); +} + +UserData::UserData(const X_USER_DATA_TYPE data_type, + const X_USER_DATA_UNION* user_data, + std::span extended_data) { + data_.type = data_type; + data_.data = *user_data; + + if (requires_additional_data()) { + extended_data_.insert(extended_data_.begin(), extended_data.begin(), + extended_data.end()); + } +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/xam/user_data.h b/src/xenia/kernel/xam/user_data.h new file mode 100644 index 000000000..32c964302 --- /dev/null +++ b/src/xenia/kernel/xam/user_data.h @@ -0,0 +1,180 @@ +/** + ****************************************************************************** + * 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_DATA_H_ +#define XENIA_KERNEL_XAM_USER_DATA_H_ + +#include +#include +#include + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +union AttributeKey { + uint32_t value; + struct { + uint32_t id : 14; + uint32_t unk : 2; + uint32_t size : 12; + uint32_t type : 4; + }; +}; + +// ToDo: Check if setting CAN be set to unset. Property can and it will be +// interpreted as a basic type property. +enum class X_USER_DATA_TYPE : uint8_t { + CONTEXT = 0, + INT32 = 1, + INT64 = 2, + DOUBLE = 3, + WSTRING = 4, + FLOAT = 5, + BINARY = 6, + DATETIME = 7, + UNSET = 0xFF, +}; + +struct X_USER_DATA_UNION { + union { + be s32; + be s64; + be u32; + be f64; + struct { + be size; + be ptr; + } unicode; + be f32; + struct { + be size; + be ptr; + } binary; + be filetime; + }; +}; +static_assert_size(X_USER_DATA_UNION, 8); + +struct alignas(8) X_USER_DATA { + X_USER_DATA_TYPE type; + X_USER_DATA_UNION data; +}; +static_assert_size(X_USER_DATA, 16); + +using UserDataTypes = std::variant>; + +constexpr uint32_t kMaxUserDataSize = 0x03E8; + +class UserData { + public: + X_USER_DATA_TYPE get_type() const { return data_.type; } + + const X_USER_DATA* get_data() const { return &data_; } + std::span get_extended_data() const { + return {extended_data_.data(), extended_data_.size()}; + } + + bool is_valid_type() const { + return data_.type >= X_USER_DATA_TYPE::CONTEXT && + data_.type <= X_USER_DATA_TYPE::DATETIME; + } + + bool requires_additional_data() const { + return data_.type == X_USER_DATA_TYPE::BINARY || + data_.type == X_USER_DATA_TYPE::WSTRING; + } + + static X_USER_DATA_TYPE get_type(uint32_t id) { + return static_cast(id >> 28); + } + static uint16_t get_max_size(uint32_t id) { + return static_cast(id >> 16) & kMaxUserDataSize; + } + + static bool requires_additional_data(uint32_t id) { + const auto type = get_type(id); + + return type == X_USER_DATA_TYPE::BINARY || + type == X_USER_DATA_TYPE::WSTRING; + } + + static uint32_t get_valid_data_size(uint32_t id, uint32_t provided_size) { + if (!requires_additional_data(id)) { + if (provided_size == sizeof(uint32_t) || + provided_size == sizeof(uint64_t)) { + return provided_size; + } + + switch (get_type(id)) { + case X_USER_DATA_TYPE::CONTEXT: + case X_USER_DATA_TYPE::FLOAT: + case X_USER_DATA_TYPE::INT32: + return sizeof(uint32_t); + + case X_USER_DATA_TYPE::DATETIME: + case X_USER_DATA_TYPE::DOUBLE: + case X_USER_DATA_TYPE::INT64: + return sizeof(uint64_t); + } + + return sizeof(uint64_t); + } + + if (!provided_size) { + return kMaxUserDataSize; + } + + return std::min(static_cast(get_max_size(id)), provided_size); + } + + size_t get_data_size() const { + return sizeof(X_USER_DATA) + extended_data_.size(); + } + + static size_t get_data_size(uint32_t id, const X_USER_DATA* user_data) { + if (requires_additional_data(id)) { + return std::min(get_max_size(id), + static_cast(user_data->data.binary.size)); + } + + return get_max_size(id); + } + + protected: + ~UserData(); + + UserData(); + UserData(const UserData& user_data); + + // From host + UserData(X_USER_DATA_TYPE data_type, UserDataTypes user_data); + // From guest + UserData(const X_USER_DATA_TYPE data_type, const uint32_t data_max_size, + const X_USER_DATA* user_data); + + // From guest - Property specific ctor. Property transfer raw data directly. + UserData(X_USER_DATA_TYPE data_type, std::span data); + + // For data from GPD + UserData(const X_USER_DATA_TYPE data_type, const X_USER_DATA_UNION* user_data, + std::span extended_data); + + X_USER_DATA data_; + std::vector extended_data_ = {}; +}; + +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif \ No newline at end of file diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index e69383efa..4db7dfb41 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -9,254 +9,150 @@ #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 { namespace xam { UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info) - : xuid_(xuid), account_info_(*account_info) { + : xuid_(xuid), account_info_(*account_info), profile_images_() { // 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." + LoadProfileGpds(); - // 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())); + LoadProfileIcon(XTileType::kGamerTile); + LoadProfileIcon(XTileType::kGamerTileSmall); } -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()); - } - - 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)); - } +GpdInfo* UserProfile::GetGpd(const uint32_t title_id) { + return const_cast( + const_cast(this)->GetGpd(title_id)); } -UserSetting* UserProfile::GetSetting(uint32_t setting_id) { - const auto& it = settings_.find(setting_id); - if (it == settings_.end()) { +const GpdInfo* UserProfile::GetGpd(const uint32_t title_id) const { + if (title_id == kDashboardID) { + return &dashboard_gpd_; + } + + if (!games_gpd_.count(title_id)) { 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; + return &games_gpd_.at(title_id); } -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; +void UserProfile::LoadProfileGpds() { + // First load dashboard GPD because it stores all opened games + dashboard_gpd_ = LoadGpd(kDashboardID); + if (!dashboard_gpd_.IsValid()) { + dashboard_gpd_ = GpdInfoProfile(); } - properties_.push_back(*property); - return true; -} + const auto gpds_to_load = dashboard_gpd_.GetTitlesInfo(); -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; +void UserProfile::LoadProfileIcon(XTileType tile_type) { + if (!kTileFileNames.count(tile_type)) { + return; } - for (auto& entry : title_achievements->second) { - if (entry.achievement_id == id) { - return &entry; - } + const std::string path = + fmt::format("{:016X}:\\{}", xuid_, kTileFileNames.at(tile_type)); + + vfs::File* file = nullptr; + vfs::FileAction action; + + const X_STATUS result = kernel_state()->file_system()->OpenFile( + nullptr, path, vfs::FileDisposition::kOpen, vfs::FileAccess::kGenericRead, + false, true, &file, &action); + + if (result != X_STATUS_SUCCESS) { + return; } - return nullptr; + + std::vector data(file->entry()->size()); + size_t written_bytes = 0; + file->ReadSync(data.data(), file->entry()->size(), 0, &written_bytes); + file->Destroy(); + + profile_images_.insert({tile_type, data}); } -std::vector* UserProfile::GetTitleAchievements( - const uint32_t title_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 {}; } - return &title_achievements->second; + 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 {}; + } + + 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 {}; + } + + file->Destroy(); + return data; +} + +bool UserProfile::WriteGpd(const uint32_t title_id) { + const GpdInfo* gpd = GetGpd(title_id); + if (!gpd) { + return false; + } + + std::vector data = gpd->Serialize(); + + vfs::File* file = nullptr; + vfs::FileAction action; + + const std::string mounted_path = + fmt::format("{:016X}:\\{:08X}.gpd", xuid_, title_id); + + const X_STATUS result = kernel_state()->file_system()->OpenFile( + nullptr, mounted_path, vfs::FileDisposition::kOverwriteIf, + vfs::FileAccess::kGenericWrite, false, true, &file, &action); + + if (result != X_STATUS_SUCCESS) { + return false; + } + + size_t written_bytes = 0; + file->WriteSync(data.data(), data.size(), 0, &written_bytes); + file->Destroy(); + return true; } } // namespace xam diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 448698af3..cfa7c6953 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -16,63 +16,24 @@ #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/user_property.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,86 +46,33 @@ 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; +enum class XTileType { + kAchievement, + kGameIcon, + kGamerTile, + kGamerTileSmall, + kLocalGamerTile, + kLocalGamerTileSmall, + kBkgnd, + kAwardedGamerTile, + kAwardedGamerTileSmall, + kGamerTileByImageId, + kPersonalGamerTile, + kPersonalGamerTileSmall, + kGamerTileByKey, + kAvatarGamerTile, + kAvatarGamerTileSmall, + kAvatarFullBody +}; - 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; +// TODO: find filenames of other tile types that are stored in profile +static const std::map kTileFileNames = { + {XTileType::kGamerTile, "tile_64.png"}, + {XTileType::kGamerTileSmall, "tile_32.png"}, + {XTileType::kPersonalGamerTile, "tile_64.png"}, + {XTileType::kPersonalGamerTileSmall, "tile_32.png"}, + {XTileType::kAvatarGamerTile, "avtr_64.png"}, + {XTileType::kAvatarGamerTileSmall, "avtr_32.png"}, }; class UserProfile { @@ -180,39 +88,50 @@ class UserProfile { uint32_t GetSubscriptionTier() const { return account_info_.GetSubscriptionTier(); } + + std::span GetProfileIcon(XTileType icon_type) { + // Overwrite same types? + if (icon_type == XTileType::kPersonalGamerTile) { + icon_type = XTileType::kGamerTile; + } + + if (icon_type == XTileType::kPersonalGamerTileSmall) { + icon_type = XTileType::kGamerTileSmall; + } + + if (profile_images_.find(icon_type) == profile_images_.cend()) { + return {}; + } + + return {profile_images_[icon_type].data(), + profile_images_[icon_type].size()}; + } + void GetPasscode(uint16_t* passcode) const { std::memcpy(passcode, account_info_.passcode, 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::vector properties_; // Includes contexts! - std::vector properties_; + std::map> profile_images_; - void LoadSetting(UserSetting*); - void SaveSetting(UserSetting*); + GpdInfo* GetGpd(const uint32_t title_id); + const GpdInfo* GetGpd(const uint32_t title_id) const; + + void LoadProfileGpds(); + void LoadProfileIcon(XTileType tile_type); + 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_property.cc b/src/xenia/kernel/xam/user_property.cc new file mode 100644 index 000000000..da4a47ab1 --- /dev/null +++ b/src/xenia/kernel/xam/user_property.cc @@ -0,0 +1,75 @@ +/** + ****************************************************************************** + * 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/user_property.h" +#include "xenia/base/logging.h" +#include "xenia/kernel/util/shim_utils.h" + +namespace xe { +namespace kernel { +namespace xam { + +Property::Property() : UserData() {}; + +Property::Property(const Property& property) + : UserData(property), property_id_(property.property_id_) {} + +Property::Property(uint32_t property_id, UserDataTypes property_data) + : UserData(get_type(property_id), property_data) { + property_id_ = static_cast(property_id); +} + +Property::Property(uint32_t property_id, uint32_t value_size, + uint8_t* value_ptr) + : UserData(get_type(property_id), + std::span(value_ptr, value_size)) { + property_id_.value = property_id; +} + +Property::Property(const uint8_t* serialized_data, size_t data_size) + : UserData(X_USER_DATA_TYPE::CONTEXT, std::span({})) {} + +Property::~Property() {}; + +std::vector Property::Serialize() const { + std::vector serialized_property(sizeof(XUSER_PROPERTY) + + extended_data_.size()); + + memcpy(serialized_property.data(), &property_id_, sizeof(AttributeKey)); + memcpy(serialized_property.data() + sizeof(AttributeKey), &data_, + sizeof(X_USER_DATA)); + + if (requires_additional_data()) { + memcpy( + serialized_property.data() + sizeof(AttributeKey) + sizeof(X_USER_DATA), + extended_data_.data(), extended_data_.size()); + } + return serialized_property; +} + +void Property::WriteToGuest(XUSER_PROPERTY* property) const { + if (!property) { + return; + } + + if (requires_additional_data()) { + property->data.type = data_.type; + property->data.data.binary.size = + static_cast(extended_data_.size()); + + memcpy(kernel_memory()->TranslateVirtual(property->data.data.binary.ptr), + extended_data_.data(), extended_data_.size()); + } else { + memcpy(&property->data, &data_, sizeof(X_USER_DATA)); + } +} + +} // namespace xam +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/util/property.h b/src/xenia/kernel/xam/user_property.h similarity index 53% rename from src/xenia/kernel/util/property.h rename to src/xenia/kernel/xam/user_property.h index 6a7119051..7aeb750af 100644 --- a/src/xenia/kernel/util/property.h +++ b/src/xenia/kernel/xam/user_property.h @@ -7,29 +7,36 @@ ****************************************************************************** */ -#ifndef XENIA_KERNEL_UTIL_PROPERTY_H_ -#define XENIA_KERNEL_UTIL_PROPERTY_H_ +#ifndef XENIA_KERNEL_XAM_USER_PROPERTY_H_ +#define XENIA_KERNEL_XAM_USER_PROPERTY_H_ #include -#include "xenia/base/byte_stream.h" -#include "xenia/kernel/util/xuserdata.h" +#include "xenia/kernel/xam/user_data.h" #include "xenia/memory.h" #include "xenia/xbox.h" namespace xe { namespace kernel { +namespace xam { + +// This structure is here because context is a one type of property. +struct XUSER_CONTEXT { + xe::be context_id; + xe::be value; +}; struct XUSER_PROPERTY { xe::be property_id; X_USER_DATA data; }; -using userDataVariant = std::variant >; - -class Property { +class Property : public UserData { public: + Property(); + Property(const Property& property); + + Property(uint32_t property_id, UserDataTypes setting_data); // Ctor used while guest is creating property. Property(uint32_t property_id, uint32_t value_size, uint8_t* value_ptr); // Ctor used for deserialization @@ -39,32 +46,15 @@ class Property { const AttributeKey GetPropertyId() const { return property_id_; } bool IsValid() const { return property_id_.value != 0; } + bool IsContext() const { return data_.type == X_USER_DATA_TYPE::CONTEXT; } + void WriteToGuest(XUSER_PROPERTY* property) const; std::vector Serialize() const; - // Writer back to guest structure - void Write(Memory* memory, XUSER_PROPERTY* property) const; - uint32_t GetSize() const { return value_size_; } - - 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; - } - - // Returns variant for specific value in LE (host) notation. - userDataVariant GetValue() const; - private: AttributeKey property_id_ = {}; - X_USER_DATA_TYPE data_type_ = X_USER_DATA_TYPE::UNSET; - - uint32_t value_size_ = 0; - std::vector value_; }; +} // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/user_settings.cc b/src/xenia/kernel/xam/user_settings.cc new file mode 100644 index 000000000..040d6e6d9 --- /dev/null +++ b/src/xenia/kernel/xam/user_settings.cc @@ -0,0 +1,127 @@ +/** + ****************************************************************************** + * 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 "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(UserSetting& setting) : UserData(setting) { + setting_id_ = setting.setting_id_; + setting_source_ = setting.setting_source_; +} + +UserSetting::UserSetting(UserSettingId setting_id, UserDataTypes setting_data) + : UserData(get_type(static_cast(setting_id)), setting_data), + setting_id_(setting_id), + setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {} + +UserSetting::UserSetting(const X_USER_PROFILE_SETTING* profile_setting) + : UserData(UserSetting::get_type(profile_setting->setting_id), + UserSetting::get_max_size(profile_setting->setting_id), + &profile_setting->data), + setting_id_( + static_cast(profile_setting->setting_id.get())), + setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {} + +UserSetting::UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting, + std::span extended_data) + : UserData(profile_setting->setting_type, &profile_setting->base_data, + extended_data), + setting_id_( + static_cast(profile_setting->setting_id.get())), + setting_source_(X_USER_PROFILE_SETTING_SOURCE::TITLE) {} + +std::optional UserSetting::GetDefaultSetting( + const UserProfile* user, uint32_t setting_id) { + const auto type = UserData::get_type(setting_id); + + switch (type) { + case X_USER_DATA_TYPE::CONTEXT: + case X_USER_DATA_TYPE::INT32: + case X_USER_DATA_TYPE::UNSET: + return std::make_optional( + static_cast(setting_id), 0); + case X_USER_DATA_TYPE::INT64: + case X_USER_DATA_TYPE::DATETIME: + return std::make_optional( + static_cast(setting_id), static_cast(0)); + case X_USER_DATA_TYPE::DOUBLE: + return std::make_optional( + static_cast(setting_id), 0.0); + case X_USER_DATA_TYPE::WSTRING: + return std::make_optional( + static_cast(setting_id), std::u16string()); + case X_USER_DATA_TYPE::FLOAT: + return std::make_optional( + static_cast(setting_id), 0.0f); + case X_USER_DATA_TYPE::BINARY: + return std::make_optional( + static_cast(setting_id), std::vector()); + default: + assert_always(); + } + + XELOGE("{}: Unknown X_USER_DATA_TYPE: {}", __func__, + static_cast(type)); + return std::nullopt; +} + +void UserSetting::WriteToGuest(X_USER_PROFILE_SETTING* setting_ptr, + uint32_t& extended_data_address) { + if (!setting_ptr) { + return; + } + + memcpy(&setting_ptr->data.data, &data_.data, sizeof(X_USER_DATA_UNION)); + setting_ptr->data.type = data_.type; + + if (requires_additional_data()) { + const auto extended_data = get_extended_data(); + + setting_ptr->data.data.binary.size = + static_cast(extended_data_.size()); + setting_ptr->data.data.binary.ptr = extended_data_address; + + memcpy(kernel_memory()->TranslateVirtual(extended_data_address), + extended_data_.data(), extended_data_.size()); + + extended_data_address += static_cast(extended_data_.size()); + } +} + +std::vector UserSetting::Serialize() 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 = data_.type; + + memcpy(&header.base_data, &data_.data, sizeof(X_USER_DATA_UNION)); + + // 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; +} + +} // 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..e5e31fe86 --- /dev/null +++ b/src/xenia/kernel/xam/user_settings.h @@ -0,0 +1,455 @@ +/** + ****************************************************************************** + * 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/xam/profile_manager.h" +#include "xenia/kernel/xam/user_data.h" +#include "xenia/kernel/xam/user_profile.h" + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace xam { + +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, kMaxUserDataSize, 0x43), // 0x43E80043, + + XPROFILE_CRUX_BIO = SettingKey(X_USER_DATA_TYPE::WSTRING, kMaxUserDataSize, + 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, kMaxUserDataSize, + 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, kMaxUserDataSize, 0x44), // 0x63E80044, + XPROFILE_GAMERCARD_AVATAR_INFO_2 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x45), // 0x63E80045, + XPROFILE_GAMERCARD_PARTY_INFO = + SettingKey(X_USER_DATA_TYPE::BINARY, 0x100, 0x46), // 0x61000046, + + XPROFILE_TITLE_SPECIFIC1 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x3FFF), // 0x63E83FFF, + XPROFILE_TITLE_SPECIFIC2 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x3FFE), // 0x63E83FFE, + XPROFILE_TITLE_SPECIFIC3 = SettingKey( + X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 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 +}; + +class UserSetting : public UserData { + public: + UserSetting(UserSetting& setting); + // Ctor for writing from host + UserSetting(UserSettingId setting_id, UserDataTypes 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); + + static std::optional GetDefaultSetting(const UserProfile* user, + uint32_t setting_id); + void WriteToGuest(X_USER_PROFILE_SETTING* setting_ptr, + uint32_t& extended_data_address); + std::vector Serialize() const; + + uint32_t get_setting_id() const { return static_cast(setting_id_); } + X_USER_PROFILE_SETTING_SOURCE get_setting_source() const { + return setting_source_; + } + + 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(); + } + + private: + UserSettingId setting_id_; + X_USER_PROFILE_SETTING_SOURCE setting_source_; + + 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_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..0deb4f728 --- /dev/null +++ b/src/xenia/kernel/xam/user_tracker.cc @@ -0,0 +1,781 @@ +/** + ****************************************************************************** + * 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/emulator.h" +#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_data.h" +#include "xenia/kernel/xam/user_property.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!", __func__); + return false; + } + + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + if (!spa_data_) { + XELOGW("{}: Missing title SPA.", __func__); + return false; + } + + const auto spa_achievement = spa_data_->GetAchievement(achievement_id); + if (!spa_achievement) { + XELOGW("{}: Missing achievement data in SPA.", __func__); + 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) { + XELOGW( + "{}: Missing achievement data in title GPD. (User: {} Title: {:08X})", + __func__, user->name(), spa_data_->title_id()); + 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::FlushUserData(const uint64_t xuid) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + user->WriteGpd(kDashboardID); + + if (spa_data_) { + user->WriteGpd(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 = user->dashboard_gpd_.GetTitleName(title_id); + info.icon = game_gpd->second.GetImage(kXdbfIdTitle); + + if (title_data->last_played.is_valid()) { + info.last_played = chrono::WinSystemClock::to_local( + 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.flags = title_data->flags; + info.all_avatar_awards = title_data->all_avatar_awards; + info.male_avatar_awards = title_data->male_avatar_awards; + info.female_avatar_awards = title_data->female_avatar_awards; + info.online_unlocked_achievements = title_data->online_achievement_count; + info.title_name = user->dashboard_gpd_.GetTitleName(title_data->title_id); + + if (title_data->last_played.is_valid()) { + info.last_played = chrono::WinSystemClock::to_local( + 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::UpdateMissingAchievemntsIcons() { + 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; + } + + for (const auto& id : game_gpd->second.GetAchievementsIds()) { + const auto entry = game_gpd->second.GetAchievementEntry(id); + + if (!entry) { + continue; + } + + if (!entry->is_achievement_unlocked()) { + continue; + } + + if (!game_gpd->second.GetImage(entry->image_id).empty()) { + continue; + } + + game_gpd->second.AddImage(entry->image_id, + spa_data_->GetIcon(entry->image_id)); + } + user->WriteGpd(spa_data_->title_id()); + } +} + +void UserTracker::UpdateSpaInfo(SpaInfo* spa_info) { + spa_data_ = spa_info; + + if (!spa_data_) { + return; + } + + UpdateProfileGpd(); + UpdateTitleGpdFile(); + UpdateMissingAchievemntsIcons(); +} + +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) { + X_XDBF_GPD_TITLE_PLAYED 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) { + return {}; + } + + return GetIcon(xuid, title_id, XTileType::kAchievement, entry->image_id); +} + +void UserTracker::AddProperty(const uint64_t xuid, const Property* property) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + const auto property_id = property->GetPropertyId(); + + if (property->IsContext()) { + const auto context_data = spa_data_->GetContext(property_id.value); + if (!context_data) { + return; + } + } else { + const auto property_data = spa_data_->GetProperty(property_id.value); + if (!property_data) { + return; + } + } + + auto entry = std::find_if(user->properties_.begin(), user->properties_.end(), + [property_id](const Property& property_data) { + return property_data.GetPropertyId().value == + property_id.value; + }); + + if (entry != user->properties_.end()) { + *entry = *property; + return; + } + + user->properties_.push_back(*property); +} + +X_STATUS UserTracker::GetProperty(const uint64_t xuid, uint32_t* property_size, + XUSER_PROPERTY* property) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return X_E_NOTFOUND; + } + + *property_size = 0; + const auto property_id = property->property_id; + + const auto entry = + std::find_if(user->properties_.cbegin(), user->properties_.cend(), + [property_id](const Property& property_data) { + return property_data.GetPropertyId().value == property_id; + }); + + if (entry == user->properties_.cend()) { + return X_E_INVALIDARG; + } + + if (entry->requires_additional_data()) { + if (!property->data.data.binary.ptr) { + return X_E_INVALIDARG; + } + } + + *property_size = static_cast(entry->get_data_size()); + entry->WriteToGuest(property); + return X_E_SUCCESS; +} + +const Property* UserTracker::GetProperty(const uint64_t xuid, + const uint32_t id) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return nullptr; + } + + const auto entry = + std::find_if(user->properties_.cbegin(), user->properties_.cend(), + [id](const Property& property_data) { + return property_data.GetPropertyId().value == id; + }); + + if (entry == user->properties_.cend()) { + return nullptr; + } + + return &(*entry); +} + +std::optional UserTracker::GetGpdSetting( + UserProfile* user, uint32_t title_id, 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(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::GetSetting(UserProfile* user, + uint32_t title_id, + uint32_t setting_id) const { + auto gpd_setting = GetGpdSetting(user, title_id, setting_id); + if (gpd_setting) { + return gpd_setting.value(); + } + + return UserSetting::GetDefaultSetting(user, setting_id); +} + +bool UserTracker::GetUserSetting(uint64_t xuid, uint32_t title_id, + 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 setting = GetSetting(user, title_id, setting_id); + if (!setting) { + return false; + } + setting_ptr->setting_id = setting_id; + setting_ptr->source = setting->get_setting_source(); + + setting->WriteToGuest(setting_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; + } + + if (value > context_data->max_value) { + return; + } + + const auto entry = + std::find_if(user->properties_.begin(), user->properties_.end(), + [id](const Property& property_data) { + return property_data.IsContext() && + property_data.GetPropertyId().value == id; + }); + + if (entry != user->properties_.cend()) { + *entry = Property(id, value); + return; + } + + user->properties_.push_back(Property(id, 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; + } + + const auto entry = std::find_if( + user->properties_.cbegin(), user->properties_.cend(), + [id](const Property& property_data) { + return property_data.get_type() == X_USER_DATA_TYPE::CONTEXT && + property_data.GetPropertyId().value == id; + }); + + if (entry == user->properties_.cend()) { + return std::nullopt; + } + + return entry->get_data()->data.u32; +} + +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 = user->GetGpd(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 = user->GetGpd(title_id); + if (!info) { + return; + } + + info->UpsertSetting(setting); + FlushUserData(xuid); +} + +std::span UserTracker::GetIcon(uint64_t xuid, uint32_t title_id, + XTileType tile_type, + uint64_t tile_id) const { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + if (!title_id) { + if (kernel_state()->emulator()->is_title_open()) { + title_id = kernel_state()->title_id(); + } + } + + switch (tile_type) { + case XTileType::kAchievement: { + if (title_id == kernel_state()->title_id()) { + return spa_data_->GetIcon(tile_id); + } else { + const auto gpd = user->GetGpd(title_id); + if (!gpd) { + return {}; + } + + return gpd->GetImage(tile_id); + } + } + case XTileType::kGameIcon: { + const auto gpd = user->GetGpd(title_id); + if (!gpd) { + return {}; + } + + return gpd->GetImage(tile_id); + } + case XTileType::kGamerTile: + case XTileType::kGamerTileSmall: + case XTileType::kPersonalGamerTile: + case XTileType::kPersonalGamerTileSmall: + return user->GetProfileIcon(tile_type); + + default: + XELOGW("{}: Unsupported tile_type: {:08X} for title: {:08X} Id: {:16X}", + __func__, static_cast(tile_type), title_id, tile_id); + } + + return {}; +} + +void UserTracker::RefreshTitleSummary(uint64_t xuid, uint32_t title_id) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return; + } + + auto profile_gpd = user->GetGpd(kDashboardID); + if (!profile_gpd) { + return; + } + + const auto title_gpd = + reinterpret_cast(user->GetGpd(title_id)); + if (!title_gpd) { + return; + } + + auto title_data = user->dashboard_gpd_.GetTitleInfo(title_id); + if (!title_data) { + return; + } + + title_data->achievements_count = title_gpd->GetAchievementCount(); + title_data->achievements_unlocked = title_gpd->GetUnlockedAchievementCount(); + title_data->gamerscore_total = title_gpd->GetTotalGamerscore(); + title_data->gamerscore_earned = title_gpd->GetGamerscore(); + + user->WriteGpd(kDashboardID); +} + +} // 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..d0eb31dc4 --- /dev/null +++ b/src/xenia/kernel/xam/user_tracker.h @@ -0,0 +1,123 @@ +/** + ****************************************************************************** + * 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::u16string title_name; + uint32_t id; + uint32_t unlocked_achievements_count; + uint32_t achievements_count; + uint32_t title_earned_gamerscore; + uint32_t gamerscore_amount; + uint32_t flags; + uint16_t online_unlocked_achievements; + X_XDBF_AVATARAWARDS_COUNTER all_avatar_awards; + X_XDBF_AVATARAWARDS_COUNTER male_avatar_awards; + X_XDBF_AVATARAWARDS_COUNTER female_avatar_awards; + 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); + void RefreshTitleSummary(uint64_t xuid, uint32_t title_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); + X_STATUS GetProperty(const uint64_t xuid, uint32_t* property_size, + XUSER_PROPERTY* property); + const Property* GetProperty(const uint64_t xuid, const uint32_t id) const; + + // Settings + void UpsertSetting(uint64_t xuid, uint32_t title_id, + const UserSetting* setting); + + bool GetUserSetting(uint64_t xuid, uint32_t title_id, 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; + + // Images + std::span GetIcon(uint64_t xuid, uint32_t title_id, + XTileType tile_type, uint64_t tile_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 GetSetting(UserProfile* user, uint32_t title_id, + uint32_t setting_id) const; + std::optional GetGpdSetting(UserProfile* user, uint32_t title_id, + uint32_t setting_id) const; + + void AddTitleToPlayedList(uint64_t xuid); + void UpdateTitleGpdFile(); + void UpdateProfileGpd(); + void UpdateMissingAchievemntsIcons(); + + void FlushUserData(const uint64_t xuid); + + 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..e2a686962 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" @@ -499,36 +500,6 @@ class GamertagModifyDialog final : public ui::ImGuiDialog { ProfileManager* profile_manager_; }; -struct AchievementInfo { - uint32_t id; - std::u16string name; - std::u16string desc; - std::u16string unachieved; - uint32_t gamerscore; - uint32_t image_id; - uint32_t flags; - std::chrono::local_time unlock_time; - - bool IsUnlocked() const { - return (flags & static_cast(AchievementFlags::kAchieved)) || - flags & static_cast(AchievementFlags::kAchievedOnline); - } - - // Unlocked online means that unlock time is confirmed and valid! - bool IsUnlockedOnline() const { - return (flags & static_cast(AchievementFlags::kAchievedOnline)); - } -}; - -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, @@ -546,98 +517,97 @@ class GameAchievementsDialog final : public XamDialog { bool LoadAchievementsData() { xe::ui::IconsData data; - const auto title_achievements = + achievements_info_ = kernel_state() ->xam_state() ->achievement_manager() ->GetTitleAchievements(profile_->xuid(), title_info_.id); - const auto title_gpd = kernel_state()->title_xdbf(); - - if (!title_achievements) { + if (achievements_info_.empty()) { return false; } - 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()); + for (const Achievement& entry : achievements_info_) { + const auto icon = + kernel_state() + ->xam_state() + ->achievement_manager() + ->GetAchievementIcon(profile_->xuid(), title_info_.id, + entry.achievement_id); - info.flags = entry.flags; - info.gamerscore = entry.gamerscore; - info.image_id = entry.image_id; - info.unlock_time = {}; - - if (entry.IsUnlocked()) { - info.unlock_time = - chrono::WinSystemClock::to_local(entry.unlock_time.to_time_point()); - } - - achievements_info_.insert({info.id, info}); - - const auto& icon_entry = - title_gpd.GetEntry(util::XdbfSection::kImage, info.image_id); - - data.insert({info.image_id, - std::make_pair(icon_entry.buffer, - static_cast(icon_entry.size))}); + data.insert({entry.image_id, icon}); } achievements_icons_ = imgui_drawer()->LoadIcons(data); return true; } - std::string GetAchievementTitle(const AchievementInfo& achievement_entry) { + std::string GetAchievementTitle(const Achievement& achievement_entry) const { std::string title = "Secret trophy"; if (achievement_entry.IsUnlocked() || show_locked_info_ || achievement_entry.flags & static_cast(AchievementFlags::kShowUnachieved)) { - title = xe::to_utf8(achievement_entry.name); + title = xe::to_utf8(achievement_entry.achievement_name); } return title; } std::string GetAchievementDescription( - const AchievementInfo& achievement_entry) { + const Achievement& achievement_entry) const { std::string description = "Hidden description"; if (achievement_entry.flags & static_cast(AchievementFlags::kShowUnachieved)) { - description = xe::to_utf8(achievement_entry.unachieved); + description = xe::to_utf8(achievement_entry.locked_description); } if (achievement_entry.IsUnlocked() || show_locked_info_) { - description = xe::to_utf8(achievement_entry.desc); + description = xe::to_utf8(achievement_entry.unlocked_description); } return description; } + ui::ImmediateTexture* GetIcon(const Achievement& achievement_entry) const { + if (!achievement_entry.IsUnlocked() && !show_locked_info_) { + return imgui_drawer()->GetLockedAchievementIcon(); + } + + if (achievements_icons_.count(achievement_entry.image_id)) { + return achievements_icons_.at(achievement_entry.image_id).get(); + } + + if (achievement_entry.IsUnlocked()) { + return nullptr; + } + return imgui_drawer()->GetLockedAchievementIcon(); + } + + std::string GetUnlockedTime(const Achievement& achievement_entry) const { + if (achievement_entry.IsUnlockedOnline()) { + const auto unlock_time = chrono::WinSystemClock::to_local( + achievement_entry.unlock_time.to_time_point()); + + return fmt::format("Unlocked: {:%Y-%m-%d %H:%M}", unlock_time); + } + + if (achievement_entry.unlock_time.is_valid()) { + const auto unlock_time = chrono::WinSystemClock::to_local( + achievement_entry.unlock_time.to_time_point()); + + return fmt::format("Unlocked: Offline ({:%Y-%m-%d %H:%M})", unlock_time); + } + return fmt::format("Unlocked: Offline"); + } + void DrawTitleAchievementInfo(ImGuiIO& io, - const AchievementInfo& achievement_entry) { + const Achievement& achievement_entry) const { const auto start_drawing_pos = ImGui::GetCursorPos(); ImGui::TableSetColumnIndex(0); - if (achievement_entry.IsUnlocked() || show_locked_info_) { - if (achievements_icons_.count(achievement_entry.image_id)) { - ImGui::Image(achievements_icons_.at(achievement_entry.image_id).get(), - default_image_icon_size); - } else { - // Case when for whatever reason there is no icon available. - ImGui::Image(0, default_image_icon_size); - } - } else { - ImGui::Image(imgui_drawer()->GetLockedAchievementIcon(), - default_image_icon_size); - } - + ImGui::Image(GetIcon(achievement_entry), default_image_icon_size); ImGui::TableNextColumn(); ImGui::PushFont(imgui_drawer()->GetTitleFont()); @@ -654,13 +624,7 @@ class GameAchievementsDialog final : public XamDialog { ImGui::GetTextLineHeight()); if (achievement_entry.IsUnlocked()) { - if (achievement_entry.IsUnlockedOnline()) { - ImGui::TextUnformatted(fmt::format("Unlocked: {:%Y-%m-%d %H:%M}", - achievement_entry.unlock_time) - .c_str()); - } else { - ImGui::TextUnformatted(fmt::format("Unlocked: Locally").c_str()); - } + ImGui::Text("%s", GetUnlockedTime(achievement_entry).c_str()); } ImGui::TableNextColumn(); @@ -691,11 +655,13 @@ class GameAchievementsDialog final : public XamDialog { bool dialog_open = true; - if (!ImGui::Begin( - fmt::format("{} Achievements List", title_info_.title_name).c_str(), - &dialog_open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_HorizontalScrollbar)) { + if (!ImGui::Begin(fmt::format("{} Achievements List", + xe::to_utf8(title_info_.title_name)) + .c_str(), + &dialog_open, + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_HorizontalScrollbar)) { Close(); ImGui::End(); return; @@ -709,7 +675,7 @@ class GameAchievementsDialog final : public XamDialog { } else { if (ImGui::BeginTable("", 3, ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) { - for (const auto& [_, entry] : achievements_info_) { + for (const auto& entry : achievements_info_) { ImGui::TableNextRow(0, default_image_icon_size.y); DrawTitleAchievementInfo(io, entry); } @@ -734,7 +700,7 @@ class GameAchievementsDialog final : public XamDialog { const TitleInfo title_info_; const UserProfile* profile_; - std::map achievements_info_; + std::vector achievements_info_; std::map> achievements_icons_; }; @@ -745,6 +711,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog { : ui::ImGuiDialog(imgui_drawer), drawing_position_(drawing_position), profile_(profile), + profile_manager_(kernel_state()->xam_state()->profile_manager()), dialog_name_(fmt::format("{}'s Games List", profile->name())) { LoadProfileGameInfo(imgui_drawer, profile); } @@ -754,43 +721,20 @@ 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; } - - 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) { + void DrawTitleEntry(ImGuiIO& io, TitleInfo& entry) { const auto start_position = ImGui::GetCursorPos(); const ImVec2 next_window_position = ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f, @@ -804,7 +748,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog { // Second Column ImGui::TableNextColumn(); ImGui::PushFont(imgui_drawer()->GetTitleFont()); - ImGui::TextUnformatted(entry.title_name.c_str()); + ImGui::TextUnformatted(xe::to_utf8(entry.title_name).c_str()); ImGui::PopFont(); ImGui::TextUnformatted( @@ -816,18 +760,73 @@ 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"); + } + ImGui::TableNextColumn(); + + 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, + if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(), + selected_title_ == entry.id, ImGuiSelectableFlags_SpanAllColumns, - ImGui::GetContentRegionAvail())) { + end_draw_position)) { + selected_title_ = entry.id; new GameAchievementsDialog(imgui_drawer(), next_window_position, &entry, profile_); } + + if (ImGui::BeginPopupContextItem( + fmt::format("Title Menu {:08X}", entry.id).c_str())) { + selected_title_ = entry.id; + if (ImGui::MenuItem("Refresh title stats", nullptr, nullptr, true)) { + kernel_state()->xam_state()->user_tracker()->RefreshTitleSummary( + profile_->xuid(), entry.id); + + const auto title_info = + kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo( + profile_->xuid(), entry.id); + + if (title_info) { + entry = title_info.value(); + } + } + + const auto savefile_path = profile_manager_->GetProfileContentPath( + profile_->xuid(), entry.id, XContentType::kSavedGame); + + const auto dlc_path = profile_manager_->GetProfileContentPath( + 0, entry.id, XContentType::kMarketplaceContent); + + const auto tu_path = profile_manager_->GetProfileContentPath( + 0, entry.id, XContentType::kInstaller); + + if (ImGui::MenuItem("Open savefile directory", nullptr, nullptr, + std::filesystem::exists(savefile_path))) { + std::thread path_open(LaunchFileExplorer, savefile_path); + path_open.detach(); + } + if (ImGui::MenuItem("Open DLC directory", nullptr, nullptr, + std::filesystem::exists(dlc_path))) { + std::thread path_open(LaunchFileExplorer, dlc_path); + path_open.detach(); + } + if (ImGui::MenuItem("Open Title Update directory", nullptr, nullptr, + std::filesystem::exists(tu_path))) { + std::thread path_open(LaunchFileExplorer, tu_path); + path_open.detach(); + } + + ImGui::EndPopup(); + } } void OnDraw(ImGuiIO& io) override { @@ -849,13 +848,30 @@ class GamesInfoDialog final : public ui::ImGuiDialog { } if (!info_.empty()) { + if (info_.size() > 10) { + ImGui::Text("Search: "); + ImGui::SameLine(); + ImGui::InputText("##Search", title_name_filter_, + title_name_filter_size); + ImGui::Separator(); + } + if (ImGui::BeginTable("", 2, ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) { - for (const auto& [_, entry] : info_) { - ImGui::TableNextRow(0, default_image_icon_size.y); + ImGui::TableNextRow(0, default_image_icon_size.y); + for (auto& entry : info_) { + std::string filter(title_name_filter_); + if (!filter.empty()) { + bool contains_filter = + utf8::lower_ascii(xe::to_utf8(entry.title_name)) + .find(utf8::lower_ascii(filter)) != std::string::npos; + + if (!contains_filter) { + continue; + } + } DrawTitleEntry(io, entry); } - ImGui::EndTable(); } } else { @@ -882,13 +898,18 @@ class GamesInfoDialog final : public ui::ImGuiDialog { } } + static constexpr uint8_t title_name_filter_size = 15; + std::string dialog_name_ = ""; + char title_name_filter_[title_name_filter_size] = ""; + uint32_t selected_title_ = 0; const ImVec2 drawing_position_ = {}; const UserProfile* profile_; + const ProfileManager* profile_manager_; std::map> title_icon; - std::map info_; + std::vector info_; }; static dword_result_t XamShowMessageBoxUi( @@ -1543,6 +1564,7 @@ bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid, } if (ImGui::BeginPopupContextItem("Profile Menu")) { + *selected_xuid = xuid; if (user_index == XUserIndexAny) { if (ImGui::MenuItem("Login")) { profile_manager->Login(xuid); @@ -1575,7 +1597,7 @@ bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid, const bool is_signedin = profile_manager->GetProfile(xuid) != nullptr; ImGui::BeginDisabled(!is_signedin); - if (ImGui::MenuItem("Show Achievements")) { + if (ImGui::MenuItem("Show Played Titles")) { new GamesInfoDialog(imgui_drawer, next_window_position, profile_manager->GetProfile(user_index)); } @@ -1922,20 +1944,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..ac9a22056 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -7,19 +7,20 @@ ****************************************************************************** */ -#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" #include "xenia/xbox.h" +#include "third_party/stb/stb_image.h" + DECLARE_int32(user_language); namespace xe { @@ -207,22 +208,7 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, be* setting_ids, uint32_t unk, 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()); - } - } + lpvoid_t overlapped_ptr) { assert_zero(unk); // probably flags // must have at least 1 to 32 settings @@ -270,109 +256,101 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, return X_ERROR_INSUFFICIENT_BUFFER; } - auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); + auto run = [=](uint32_t& extended_error, uint32_t& length) { + auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index); - if (!user_profile && !xuids) { - if (overlapped) { - kernel_state()->CompleteOverlappedImmediate( - kernel_state()->memory()->HostToGuestVirtual(overlapped), - X_ERROR_NO_SUCH_USER); - return X_ERROR_IO_PENDING; - } - return X_ERROR_NO_SUCH_USER; - } - - if (xuids) { - uint64_t user_xuid = static_cast(xuids[0]); - if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) { - if (overlapped) { - kernel_state()->CompleteOverlappedImmediate( - kernel_state()->memory()->HostToGuestVirtual(overlapped), - X_ERROR_NO_SUCH_USER); - return X_ERROR_IO_PENDING; - } + if (!user_profile && !xuids) { return X_ERROR_NO_SUCH_USER; } - user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid); - } - if (!user_profile) { - return X_ERROR_NO_SUCH_USER; - } - - // First call asks for size (fill buffer_size_ptr). - // Second call asks for buffer contents with that size. - - // TODO(gibbed): setting validity checking without needing a user profile - // object. - 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; - XELOGE( - "xeXamUserReadProfileSettingsEx requested unimplemented setting " - "{:08X}", - setting_id); - } - } - if (any_missing) { - // TODO(benvanik): don't fail? most games don't even check! - if (overlapped) { - kernel_state()->CompleteOverlappedImmediate( - kernel_state()->memory()->HostToGuestVirtual(overlapped), - X_ERROR_INVALID_PARAMETER); - return X_ERROR_IO_PENDING; - } - return X_ERROR_INVALID_PARAMETER; - } - - auto out_header = reinterpret_cast(buffer); - auto out_setting = reinterpret_cast(&out_header[1]); - out_header->setting_count = static_cast(setting_count); - out_header->settings_ptr = - kernel_state()->memory()->HostToGuestVirtual(out_setting); - - DataByteStream out_stream( - kernel_state()->memory()->HostToGuestVirtual(buffer), buffer, buffer_size, - needed_header_size); - 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()); if (xuids) { - out_setting->xuid = user_profile->xuid(); - } else { - out_setting->xuid = -1; - out_setting->user_index = user_index; + uint64_t user_xuid = static_cast(xuids[0]); + if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) { + extended_error = X_HRESULT_FROM_WIN32(X_ERROR_NO_SUCH_USER); + length = 0; + return X_ERROR_NO_SUCH_USER; + } + user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid); } - 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); + if (!user_profile) { + extended_error = X_HRESULT_FROM_WIN32(X_ERROR_NO_SUCH_USER); + length = 0; + return X_ERROR_NO_SUCH_USER; } - ++out_setting; + + // First call asks for size (fill buffer_size_ptr). + // Second call asks for buffer contents with that size. + + bool any_missing = false; + for (uint32_t i = 0; i < setting_count; ++i) { + auto setting_id = static_cast(setting_ids[i]); + if (!UserSetting::is_setting_valid(setting_id)) { + XELOGE( + "xeXamUserReadProfileSettingsEx requested unimplemented " + "setting " + "{:08X}", + setting_id); + any_missing = true; + } + } + if (any_missing) { + extended_error = X_HRESULT_FROM_WIN32(X_ERROR_INVALID_PARAMETER); + length = 0; + return X_ERROR_INVALID_PARAMETER; + // TODO(benvanik): don't fail? most games don't even check! + } + + auto out_header = reinterpret_cast(buffer); + auto out_setting = + reinterpret_cast(&out_header[1]); + out_header->setting_count = static_cast(setting_count); + out_header->settings_ptr = + kernel_state()->memory()->HostToGuestVirtual(out_setting); + + uint32_t additional_data_buffer_ptr = + out_header->settings_ptr + + (setting_count * sizeof(X_USER_PROFILE_SETTING)); + + std::fill_n(out_setting, setting_count, X_USER_PROFILE_SETTING{}); + + for (uint32_t n = 0; n < setting_count; ++n) { + uint32_t setting_id = setting_ids[n]; + + const bool is_valid = + kernel_state()->xam_state()->user_tracker()->GetUserSetting( + user_profile->xuid(), + title_id ? title_id : kernel_state()->title_id(), setting_id, + out_setting, additional_data_buffer_ptr); + + if (is_valid) { + if (xuids) { + out_setting->xuid = user_profile->xuid(); + } else { + out_setting->user_index = user_index; + } + } + ++out_setting; + } + + extended_error = X_HRESULT_FROM_WIN32(X_STATUS_SUCCESS); + length = 0; + return X_STATUS_SUCCESS; + }; + + if (!overlapped_ptr) { + uint32_t extended_error, length; + return run(extended_error, length); } - if (overlapped) { - kernel_state()->CompleteOverlappedImmediate( - kernel_state()->memory()->HostToGuestVirtual(overlapped), - X_ERROR_SUCCESS); - return X_ERROR_IO_PENDING; - } - return X_ERROR_SUCCESS; + kernel_state()->CompleteOverlappedDeferredEx(run, overlapped_ptr); + return X_ERROR_IO_PENDING; } dword_result_t XamUserReadProfileSettings_entry( dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids, dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr, - lpvoid_t buffer_ptr, pointer_t overlapped) { + lpvoid_t buffer_ptr, lpvoid_t overlapped) { return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids, setting_count, setting_ids, 0, buffer_size_ptr, buffer_ptr, overlapped); @@ -382,7 +360,7 @@ DECLARE_XAM_EXPORT1(XamUserReadProfileSettings, kUserProfiles, kImplemented); dword_result_t XamUserReadProfileSettingsEx_entry( dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids, dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr, - dword_t unk_2, lpvoid_t buffer_ptr, pointer_t overlapped) { + dword_t unk_2, lpvoid_t buffer_ptr, lpvoid_t overlapped) { return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids, setting_count, setting_ids, unk_2, buffer_size_ptr, buffer_ptr, overlapped); @@ -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_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); } @@ -692,18 +624,57 @@ dword_result_t XamParseGamerTileKey_entry(lpdword_t key_ptr, lpdword_t out1_ptr, } DECLARE_XAM_EXPORT1(XamParseGamerTileKey, kUserProfiles, kStub); -dword_result_t XamReadTileToTexture_entry(dword_t unknown, dword_t title_id, +dword_result_t XamReadTileToTexture_entry(dword_t tile_type, dword_t title_id, qword_t tile_id, dword_t user_index, lpvoid_t buffer_ptr, dword_t stride, - dword_t height, + dword_t tile_height, dword_t overlapped_ptr) { - // TODO(gibbed): unknown=0,2,3,9 - if (!tile_id) { + if (!buffer_ptr) { return X_ERROR_INVALID_PARAMETER; } - size_t size = size_t(stride) * size_t(height); - std::memset(buffer_ptr, 0xFF, size); + size_t buffer_size = size_t(stride) * size_t(tile_height); + + auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return X_ERROR_INVALID_PARAMETER; + } + + std::span tile = + kernel_state()->xam_state()->user_tracker()->GetIcon( + user->xuid(), title_id, static_cast(tile_type.value()), + tile_id); + + if (tile.empty()) { + return X_ERROR_SUCCESS; + } + + int width, height, channels; + unsigned char* imageData = + stbi_load_from_memory(tile.data(), static_cast(tile.size()), &width, + &height, &channels, STBI_rgb_alpha); + + size_t icon_dimmension_size = width * height; + std::fill_n(reinterpret_cast(buffer_ptr.host_address()), + icon_dimmension_size * sizeof(uint32_t), 0); + + for (int i = 0; i < icon_dimmension_size; i++) { + unsigned char* pixel = &imageData[i * sizeof(uint32_t)]; + + // RGBA to ARGB. TODO: Find faster method! + // RGBA->AGBR + std::swap(pixel[0], pixel[3]); + // AGBR->ARBG + std::swap(pixel[1], pixel[3]); + // ARBG->ARGB + std::swap(pixel[2], pixel[3]); + } + + memcpy(buffer_ptr, imageData, + std::min(buffer_size, static_cast(icon_dimmension_size * + sizeof(uint32_t)))); + + stbi_image_free(imageData); if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, 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..f955ef360 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info.cc @@ -0,0 +1,309 @@ +/** + ****************************************************************************** + * 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(uint64_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) { + X_XDBF_GPD_SETTING_HEADER* setting = GetSetting(id); + + if (!setting) { + return {}; + } + + if (setting->setting_type != X_USER_DATA_TYPE::BINARY && + setting->setting_type != X_USER_DATA_TYPE::WSTRING) { + return {}; + } + + const uint32_t size = setting->base_data.binary.size; + const uint8_t* data_ptr = reinterpret_cast(setting + 1); + return {data_ptr, size}; +} + +void GpdInfo::UpsertSetting(const UserSetting* setting_data) { + const auto serialized_data = setting_data->Serialize(); + + 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); + 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..11ba0ce64 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info.h @@ -0,0 +1,158 @@ +/** + ****************************************************************************** + * 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/xam/user_data.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 +}; + +enum class AchievementFlags : uint32_t { + kTypeMask = 0x7, + kShowUnachieved = 0x8, + kAchievedOnline = 0x10000, + kAchieved = 0x20000, + kNotAchievable = 0x40000, + kWasNotAchievable = 0x80000, + kPlatformMask = 0x700000, + kColorizable = 0x1000000, // avatar awards 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; + + bool is_achievement_unlocked() const { + return flags & static_cast(AchievementFlags::kAchieved); + } +}; +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. + X_FILETIME 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]; + X_USER_DATA_UNION 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); + + // 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(uint64_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: + 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..b6220a6cf --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_profile.cc @@ -0,0 +1,114 @@ +/** + ****************************************************************************** + * 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) { + X_XDBF_GPD_TITLE_PLAYED* current_info = GetTitleInfo(title_id); + if (!current_info) { + return; + } + + memcpy(current_info, title_data, sizeof(X_XDBF_GPD_TITLE_PLAYED)); +} + +} // 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..998e763b7 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_title.cc @@ -0,0 +1,196 @@ +/** + ****************************************************************************** + * 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" +#include + +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; + + auto achievements = + entries_ | std::views::filter([](const auto& entry) { + return !IsSyncEntry(&entry); + }) | + std::views::filter([](const auto& entry) { + return IsEntryOfSection(&entry, GpdSection::kAchievement); + }); + + for (const auto& achievement : achievements) { + ids.push_back(static_cast(achievement.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); +} + +uint32_t GpdInfoTitle::GetTotalGamerscore() { + const auto ids = GetAchievementsIds(); + + uint32_t gamerscore = 0; + for (const auto id : ids) { + gamerscore += GetAchievementEntry(id)->gamerscore; + } + + return gamerscore; +} +uint32_t GpdInfoTitle::GetGamerscore() { + const auto ids = GetAchievementsIds(); + uint32_t gamerscore = 0; + for (const auto id : ids) { + const auto entry = GetAchievementEntry(id); + if (entry->is_achievement_unlocked()) { + gamerscore += GetAchievementEntry(id)->gamerscore; + } + } + return gamerscore; +} + +uint32_t GpdInfoTitle::GetAchievementCount() { + return static_cast(GetAchievementsIds().size()); +} + +uint32_t GpdInfoTitle::GetUnlockedAchievementCount() { + const auto ids = GetAchievementsIds(); + uint32_t count = 0; + for (const auto id : ids) { + const auto entry = GetAchievementEntry(id); + if (entry->is_achievement_unlocked()) { + count += 1; + } + } + return count; +} + +} // 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..02b248aaa --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/gpd_info_title.h @@ -0,0 +1,60 @@ +/** + ****************************************************************************** + * 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); + + uint32_t GetTotalGamerscore(); + uint32_t GetGamerscore(); + uint32_t GetAchievementCount(); + uint32_t GetUnlockedAchievementCount(); + + 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..f9c42c3fe --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/spa_info.cc @@ -0,0 +1,300 @@ +/** + ****************************************************************************** + * 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" + +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) { + return GetSpaEntry(achievements_, id); +} + +const XdbfContextTableEntry* SpaInfo::GetContext(uint32_t id) { + return GetSpaEntry(contexts_, id); +} + +const XdbfPropertyTableEntry* SpaInfo::GetProperty(uint32_t id) { + return GetSpaEntry(properties_, id); +} + +template +T SpaInfo::GetSpaEntry(std::vector& container, uint32_t id) { + for (const auto& entry : container) { + 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..a4de36cce --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/spa_info.h @@ -0,0 +1,223 @@ +/** + ****************************************************************************** + * 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 + +#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(); + + template + static T GetSpaEntry(std::vector& container, uint32_t id); +}; + +} // 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 f3484d219..ae5ea0469 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -245,10 +245,13 @@ std::map> ImGuiDrawer::LoadIcons( int width, height, channels; for (const auto& icon : data) { - unsigned char* image_data = - stbi_load_from_memory(icon.second.first, icon.second.second, &width, - &height, &channels, STBI_rgb_alpha); + unsigned char* image_data = stbi_load_from_memory( + icon.second.data(), static_cast(icon.second.size()), &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))); diff --git a/src/xenia/ui/imgui_drawer.h b/src/xenia/ui/imgui_drawer.h index 7b61f5208..08cbf3745 100644 --- a/src/xenia/ui/imgui_drawer.h +++ b/src/xenia/ui/imgui_drawer.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "third_party/imgui/imgui.h" @@ -34,7 +35,7 @@ class ImGuiDialog; class ImGuiNotification; class Window; -using IconsData = std::map>; +using IconsData = std::map>; class ImGuiDrawer : public WindowInputListener, public UIDrawer { public: diff --git a/src/xenia/vfs/devices/disc_image_entry.cc b/src/xenia/vfs/devices/disc_image_entry.cc index 0d251dff5..d43666ca5 100644 --- a/src/xenia/vfs/devices/disc_image_entry.cc +++ b/src/xenia/vfs/devices/disc_image_entry.cc @@ -51,5 +51,6 @@ std::unique_ptr DiscImageEntry::OpenMapped( return mmap_->Slice(real_offset, real_length); } +bool DiscImageEntry::DeleteEntryInternal(Entry* entry) { return false; } } // namespace vfs } // namespace xe diff --git a/src/xenia/vfs/devices/disc_image_entry.h b/src/xenia/vfs/devices/disc_image_entry.h index f955e345f..fd458d84f 100644 --- a/src/xenia/vfs/devices/disc_image_entry.h +++ b/src/xenia/vfs/devices/disc_image_entry.h @@ -45,6 +45,8 @@ class DiscImageEntry : public Entry { private: friend class DiscImageDevice; + bool DeleteEntryInternal(Entry* entry) override; + MappedMemory* mmap_; size_t data_offset_; size_t data_size_; diff --git a/src/xenia/vfs/devices/disc_zarchive_entry.cc b/src/xenia/vfs/devices/disc_zarchive_entry.cc index 828ab597c..ce9cfe0f8 100644 --- a/src/xenia/vfs/devices/disc_zarchive_entry.cc +++ b/src/xenia/vfs/devices/disc_zarchive_entry.cc @@ -45,5 +45,7 @@ std::unique_ptr DiscZarchiveEntry::OpenMapped( return nullptr; } +bool DiscZarchiveEntry::DeleteEntryInternal(Entry* entry) { return false; } + } // namespace vfs } // namespace xe diff --git a/src/xenia/vfs/devices/disc_zarchive_entry.h b/src/xenia/vfs/devices/disc_zarchive_entry.h index 3b01b25a9..6a19f1c8e 100644 --- a/src/xenia/vfs/devices/disc_zarchive_entry.h +++ b/src/xenia/vfs/devices/disc_zarchive_entry.h @@ -45,6 +45,8 @@ class DiscZarchiveEntry : public Entry { friend class DiscZarchiveDevice; friend class DiscZarchiveFile; + bool DeleteEntryInternal(Entry* entry) override; + uint32_t handle_; size_t data_offset_; size_t data_size_; diff --git a/src/xenia/vfs/devices/host_path_entry.cc b/src/xenia/vfs/devices/host_path_entry.cc index 8d3ede134..a7da81c50 100644 --- a/src/xenia/vfs/devices/host_path_entry.cc +++ b/src/xenia/vfs/devices/host_path_entry.cc @@ -104,10 +104,20 @@ bool HostPathEntry::DeleteEntryInternal(Entry* entry) { auto removed = std::filesystem::remove_all(full_path, ec); return removed >= 1 && removed != static_cast(-1); } else { - // Delete file only if it exists. - return !std::filesystem::is_directory(full_path) && - (!std::filesystem::exists(full_path) || - std::filesystem::remove(full_path, ec)); + // Skip directories, they we're handled above. + if (std::filesystem::is_directory(full_path)) { + return false; + } + + if (std::filesystem::exists(full_path)) { + const auto result = std::filesystem::remove(full_path, ec); + if (ec) { + XELOGE("{}: Cannot remove file entry. File: {} Error: {}", __func__, + full_path, ec.message()); + return false; + } + } + return true; } } diff --git a/src/xenia/vfs/devices/null_entry.cc b/src/xenia/vfs/devices/null_entry.cc index 45b511c40..0a6caef64 100644 --- a/src/xenia/vfs/devices/null_entry.cc +++ b/src/xenia/vfs/devices/null_entry.cc @@ -51,5 +51,7 @@ X_STATUS NullEntry::Open(uint32_t desired_access, File** out_file) { return X_STATUS_SUCCESS; } +bool NullEntry::DeleteEntryInternal(Entry* entry) { return false; } + } // namespace vfs } // namespace xe diff --git a/src/xenia/vfs/devices/null_entry.h b/src/xenia/vfs/devices/null_entry.h index 84f01cb95..50f2a7a4e 100644 --- a/src/xenia/vfs/devices/null_entry.h +++ b/src/xenia/vfs/devices/null_entry.h @@ -34,6 +34,8 @@ class NullEntry : public Entry { private: friend class NullDevice; + + bool DeleteEntryInternal(Entry* entry) override; }; } // namespace vfs diff --git a/src/xenia/vfs/devices/xcontent_container_entry.cc b/src/xenia/vfs/devices/xcontent_container_entry.cc index 1f264dfec..c3c0f9a09 100644 --- a/src/xenia/vfs/devices/xcontent_container_entry.cc +++ b/src/xenia/vfs/devices/xcontent_container_entry.cc @@ -42,5 +42,7 @@ X_STATUS XContentContainerEntry::Open(uint32_t desired_access, return X_STATUS_SUCCESS; } +bool XContentContainerEntry::DeleteEntryInternal(Entry* entry) { return false; } + } // namespace vfs } // namespace xe \ No newline at end of file diff --git a/src/xenia/vfs/devices/xcontent_container_entry.h b/src/xenia/vfs/devices/xcontent_container_entry.h index 8022a088a..563d02b4e 100644 --- a/src/xenia/vfs/devices/xcontent_container_entry.h +++ b/src/xenia/vfs/devices/xcontent_container_entry.h @@ -51,6 +51,8 @@ class XContentContainerEntry : public Entry { friend class StfsContainerDevice; friend class SvodContainerDevice; + bool DeleteEntryInternal(Entry* entry) override; + MultiFileHandles* files_; size_t data_offset_; size_t data_size_; diff --git a/src/xenia/vfs/entry.h b/src/xenia/vfs/entry.h index 3b80c25cb..4346663ec 100644 --- a/src/xenia/vfs/entry.h +++ b/src/xenia/vfs/entry.h @@ -140,7 +140,7 @@ class Entry { const std::string_view name, uint32_t attributes) { return nullptr; } - virtual bool DeleteEntryInternal(Entry* entry) { return false; } + virtual bool DeleteEntryInternal(Entry* entry) = 0; virtual void RenameEntryInternal(const std::filesystem::path file_path) {} xe::global_critical_region global_critical_region_; diff --git a/src/xenia/xbox.h b/src/xenia/xbox.h index 87bc5455e..c73017283 100644 --- a/src/xenia/xbox.h +++ b/src/xenia/xbox.h @@ -13,6 +13,7 @@ #include #include +#include "xenia/base/chrono.h" #include "xenia/base/memory.h" #include "xenia/base/string.h" @@ -463,6 +464,48 @@ struct X_KSPINLOCK { xe::be prcb_of_owner; }; static_assert_size(X_KSPINLOCK, 4); + +struct X_FILETIME { + static constexpr uint64_t minimal_valid_time = 125911584000000000; + static constexpr uint64_t maximal_valid_time = 157469184000000000; + + xe::be high_part; + xe::be low_part; + + X_FILETIME() { + high_part = 0; + low_part = 0; + } + + X_FILETIME(uint64_t filetime) { + high_part = static_cast(filetime >> 32); + low_part = static_cast(filetime); + } + + X_FILETIME(std::time_t time) { + const auto file_time = + chrono::WinSystemClock::to_file_time(chrono::WinSystemClock::from_sys( + std::chrono::system_clock::from_time_t(time))); + + high_part = static_cast(file_time >> 32); + low_part = static_cast(file_time); + } + + chrono::WinSystemClock::time_point to_time_point() const { + const uint64_t filetime = + (static_cast(high_part) << 32) | low_part; + + return chrono::WinSystemClock::from_file_time(filetime); + } + + bool is_valid() const { + const uint64_t filetime = + (static_cast(high_part) << 32) | low_part; + + return filetime >= minimal_valid_time && filetime <= maximal_valid_time; + } +}; +static_assert_size(X_FILETIME, 0x8); #pragma pack(pop) // Found by dumping the kSectionStringTable sections of various games: @@ -487,6 +530,7 @@ enum class XLanguage : uint32_t { }; enum class XContentType : uint32_t { + kInvalid = 0x00000000, kSavedGame = 0x00000001, kMarketplaceContent = 0x00000002, kPublisher = 0x00000003,