From 86bd62125d621f9b3ab773d846924a6464fe3c53 Mon Sep 17 00:00:00 2001 From: Gliniak Date: Fri, 22 Mar 2024 11:14:31 +0100 Subject: [PATCH] [Kernel] Implemented Game Info Database This aggregates XDBF and XLAST into one entity. Replaced UserData::Key into more versatile AttributeKey for future usage in properties --- .gitmodules | 6 + premake5.lua | 2 + src/xenia/emulator.cc | 53 ++-- src/xenia/emulator.h | 3 + src/xenia/kernel/premake5.lua | 2 + src/xenia/kernel/util/game_info_database.cc | 285 ++++++++++++++++++++ src/xenia/kernel/util/game_info_database.h | 135 ++++++++++ src/xenia/kernel/util/xlast.cc | 248 +++++++++++++++++ src/xenia/kernel/util/xlast.h | 100 +++++++ src/xenia/kernel/util/xuserdata.h | 10 + src/xenia/kernel/xam/user_profile.h | 2 +- src/xenia/kernel/xam/xam_user.cc | 2 +- third_party/pugixml | 1 + third_party/pugixml.lua | 10 + third_party/zlib | 1 + third_party/zlib.lua | 9 + 16 files changed, 840 insertions(+), 29 deletions(-) create mode 100644 src/xenia/kernel/util/game_info_database.cc create mode 100644 src/xenia/kernel/util/game_info_database.h create mode 100644 src/xenia/kernel/util/xlast.cc create mode 100644 src/xenia/kernel/util/xlast.h create mode 160000 third_party/pugixml create mode 100644 third_party/pugixml.lua create mode 160000 third_party/zlib create mode 100644 third_party/zlib.lua diff --git a/.gitmodules b/.gitmodules index a5388af69..2cd0d30ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -97,3 +97,9 @@ [submodule "third_party/rapidcsv"] path = third_party/rapidcsv url = https://github.com/d99kris/rapidcsv +[submodule "third_party/zlib"] + path = third_party/zlib + url = https://github.com/madler/zlib.git +[submodule "third_party/pugixml"] + path = third_party/pugixml + url = https://github.com/zeux/pugixml.git diff --git a/premake5.lua b/premake5.lua index 1faedd90c..437b13fe5 100644 --- a/premake5.lua +++ b/premake5.lua @@ -262,6 +262,8 @@ workspace("xenia") include("third_party/xxhash.lua") include("third_party/zarchive.lua") include("third_party/zstd.lua") + include("third_party/zlib.lua") + include("third_party/pugixml.lua") if not os.istarget("android") then -- SDL2 requires sdl2-config, and as of November 2020 isn't high-quality on diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 0f18599de..0184524bc 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -120,6 +120,7 @@ Emulator::Emulator(const std::filesystem::path& command_line, kernel_state_(), main_thread_(), title_id_(std::nullopt), + game_info_database_(), paused_(false), restoring_(false), restore_fence_() { @@ -1257,10 +1258,12 @@ 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); - if (db.is_valid()) { - XLanguage language = - db.GetExistingLanguage(static_cast(cvars::user_language)); - title_name_ = db.title(language); + + game_info_database_ = std::make_unique(&db); + + if (game_info_database_->IsValid()) { + title_name_ = game_info_database_->GetTitleName( + static_cast(cvars::user_language)); XELOGI("Title name: {}", title_name_); // Show achievments data @@ -1268,48 +1271,44 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, table.format().multi_byte_characters(true); table.add_row({"ID", "Title", "Description", "Gamerscore"}); - const std::vector - achievement_list = db.GetAchievements(); - for (const kernel::util::XdbfAchievementTableEntry& entry : + const std::vector + achievement_list = game_info_database_->GetAchievements(); + for (const kernel::util::GameInfoDatabase::Achievement& entry : achievement_list) { - std::string label = string_util::remove_eol(string_util::trim( - db.GetStringTableEntry(language, entry.label_id))); - std::string desc = string_util::remove_eol(string_util::trim( - db.GetStringTableEntry(language, entry.description_id))); - - table.add_row({fmt::format("{}", entry.id), label, desc, - fmt::format("{}", entry.gamerscore)}); + table.add_row({fmt::format("{}", entry.id), entry.label, + entry.description, fmt::format("{}", entry.gamerscore)}); } XELOGI("-------------------- ACHIEVEMENTS --------------------\n{}", table.str()); - const std::vector properties_list = - db.GetProperties(); + const std::vector + properties_list = game_info_database_->GetProperties(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "Name", "Data Size"}); - for (const kernel::util::XdbfPropertyTableEntry& entry : + for (const kernel::util::GameInfoDatabase::Property& entry : properties_list) { - std::string label = string_util::remove_eol(string_util::trim( - db.GetStringTableEntry(language, entry.string_id))); + std::string label = + string_util::remove_eol(string_util::trim(entry.description)); table.add_row({fmt::format("{:08X}", entry.id), label, fmt::format("{}", entry.data_size)}); } XELOGI("-------------------- PROPERTIES --------------------\n{}", table.str()); - const std::vector contexts_list = - db.GetContexts(); + const std::vector contexts_list = + game_info_database_->GetContexts(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "Name", "Default Value", "Max Value"}); - for (const kernel::util::XdbfContextTableEntry& entry : contexts_list) { - std::string label = string_util::remove_eol(string_util::trim( - db.GetStringTableEntry(language, entry.string_id))); + for (const kernel::util::GameInfoDatabase::Context& entry : + contexts_list) { + std::string label = + string_util::remove_eol(string_util::trim(entry.description)); table.add_row({fmt::format("{:08X}", entry.id), label, fmt::format("{}", entry.default_value), fmt::format("{}", entry.max_value)}); @@ -1317,9 +1316,9 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, XELOGI("-------------------- CONTEXTS --------------------\n{}", table.str()); - auto icon_block = db.icon(); - if (icon_block) { - display_window_->SetIcon(icon_block.buffer, icon_block.size); + auto icon_block = game_info_database_->GetIcon(); + if (!icon_block.empty()) { + display_window_->SetIcon(icon_block.data(), icon_block.size()); } } } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 04bd98058..350e2ca02 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -20,6 +20,8 @@ #include "xenia/base/delegate.h" #include "xenia/base/exception_handler.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/game_info_database.h" +#include "xenia/kernel/util/xlast.h" #include "xenia/memory.h" #include "xenia/patcher/patcher.h" #include "xenia/patcher/plugin_loader.h" @@ -318,6 +320,7 @@ class Emulator { kernel::object_ref main_thread_; kernel::object_ref plugin_loader_thread_; std::optional title_id_; // Currently running title ID + std::unique_ptr game_info_database_; bool paused_; bool restoring_; diff --git a/src/xenia/kernel/premake5.lua b/src/xenia/kernel/premake5.lua index 1061c24ea..bd00330bc 100644 --- a/src/xenia/kernel/premake5.lua +++ b/src/xenia/kernel/premake5.lua @@ -9,6 +9,8 @@ project("xenia-kernel") links({ "aes_128", "fmt", + "zlib", + "pugixml", "xenia-apu", "xenia-base", "xenia-cpu", diff --git a/src/xenia/kernel/util/game_info_database.cc b/src/xenia/kernel/util/game_info_database.cc new file mode 100644 index 000000000..118a397b8 --- /dev/null +++ b/src/xenia/kernel/util/game_info_database.cc @@ -0,0 +1,285 @@ +/** + ****************************************************************************** + * 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/game_info_database.h" +#include "xenia/base/logging.h" + +namespace xe { +namespace kernel { +namespace util { + +GameInfoDatabase::GameInfoDatabase(const XdbfGameData* data) { + if (!data) { + return; + } + + if (!data->is_valid()) { + return; + } + + 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); + + if (!xlast_ptr) { + XELOGW( + "GameDatabase: Title doesn't contain XLAST data! Multiplayer " + "functionality might be limited."); + return; + } + + xlast_gamedata_ = + std::make_unique(xlast_ptr, compressed_size, decompressed_size); + + if (!xlast_gamedata_) { + XELOGW( + "GameDatabase: Title XLAST data is corrupted! Multiplayer " + "functionality might be limited."); + return; + } +} + +GameInfoDatabase::~GameInfoDatabase() {} + +std::string GameInfoDatabase::GetTitleName(const XLanguage language) const { + if (!is_valid_) { + return ""; + } + + return xdbf_gamedata_->title(xdbf_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); + return data; +} + +XLanguage GameInfoDatabase::GetDefaultLanguage() const { + if (!is_valid_) { + return XLanguage::kEnglish; + } + + return xdbf_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); +} + +GameInfoDatabase::Context GameInfoDatabase::GetContext( + const uint32_t id) const { + Context context = {}; + + if (!is_valid_) { + return context; + } + + const auto xdbf_context = xdbf_gamedata_->GetContext(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; +} + +GameInfoDatabase::Property GameInfoDatabase::GetProperty( + const uint32_t id) const { + Property property = {}; + + if (!is_valid_) { + return property; + } + + const auto xdbf_property = xdbf_gamedata_->GetProperty(id); + + property.id = xdbf_property.id; + property.data_size = xdbf_property.data_size; + property.description = GetLocalizedString(xdbf_property.string_id); + return property; +} + +GameInfoDatabase::Achievement GameInfoDatabase::GetAchievement( + const uint32_t id) const { + Achievement achievement = {}; + + if (!is_valid_) { + return achievement; + } + + const auto xdbf_achievement = xdbf_gamedata_->GetAchievement(id); + + 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.unachieved_description = + GetLocalizedString(xdbf_achievement.unachieved_id); + return achievement; +} + +std::vector GameInfoDatabase::GetMatchmakingAttributes( + const uint32_t id) const { + 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; +} + +// XLAST +GameInfoDatabase::Query GameInfoDatabase::GetQueryData( + const uint32_t id) const { + Query query = {}; + + if (!xlast_gamedata_) { + return query; + } + + const auto xlast_query = xlast_gamedata_->GetMatchmakingQuery(id); + if (!xlast_query) { + return query; + } + + query.id = id; + query.name = xlast_query->GetName(); + query.input_parameters = xlast_query->GetParameters(); + query.filters = xlast_query->GetFilters(); + query.expected_return = xlast_query->GetReturns(); + return query; +} + +std::vector GameInfoDatabase::GetSupportedLanguages() const { + if (!xlast_gamedata_) { + return {}; + } + + return xlast_gamedata_->GetSupportedLanguages(); +} + +GameInfoDatabase::ProductInformation GameInfoDatabase::GetProductInformation() + const { + if (!xlast_gamedata_) { + return {}; + } + + ProductInformation info = {}; + + const auto attributes = xlast_gamedata_->GetProductInformationAttributes(); + for (const auto& attribute : attributes) { + switch (attribute.first) { + case ProductInformationEntry::MaxOfflinePlayers: + info.max_offline_players_count = attribute.second; + break; + case ProductInformationEntry::MaxSystemLinkPlayers: + info.max_systemlink_players_count = attribute.second; + break; + case ProductInformationEntry::MaxLivePlayers: + info.max_live_players_count = attribute.second; + break; + case ProductInformationEntry::PublisherString: + info.publisher_name = xe::to_utf8(xlast_gamedata_->GetLocalizedString( + attribute.second, XLanguage::kEnglish)); + break; + case ProductInformationEntry::DeveloperString: + info.developer_name = xe::to_utf8(xlast_gamedata_->GetLocalizedString( + attribute.second, XLanguage::kEnglish)); + break; + case ProductInformationEntry::MarketingString: + info.marketing_info = xe::to_utf8(xlast_gamedata_->GetLocalizedString( + attribute.second, XLanguage::kEnglish)); + break; + case ProductInformationEntry::GenreTypeString: + info.genre_description = + xe::to_utf8(xlast_gamedata_->GetLocalizedString( + attribute.second, XLanguage::kEnglish)); + break; + default: + break; + } + } + return info; +} + +// Aggregators +std::vector GameInfoDatabase::GetContexts() const { + std::vector contexts; + + if (!is_valid_) { + return contexts; + } + + const auto xdbf_contexts = xdbf_gamedata_->GetContexts(); + for (const auto& entry : xdbf_contexts) { + contexts.push_back(GetContext(entry.id)); + } + + return contexts; +} + +std::vector GameInfoDatabase::GetProperties() + const { + std::vector properties; + + if (!is_valid_) { + return properties; + } + + const auto xdbf_properties = xdbf_gamedata_->GetProperties(); + for (const auto& entry : xdbf_properties) { + properties.push_back(GetProperty(entry.id)); + } + + return properties; +} + +std::vector GameInfoDatabase::GetAchievements() + const { + std::vector achievements; + + if (!is_valid_) { + return achievements; + } + + const auto xdbf_achievements = xdbf_gamedata_->GetAchievements(); + for (const auto& entry : xdbf_achievements) { + achievements.push_back(GetAchievement(entry.id)); + } + + return achievements; +} + +} // namespace util +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/util/game_info_database.h b/src/xenia/kernel/util/game_info_database.h new file mode 100644 index 000000000..5d3c608f6 --- /dev/null +++ b/src/xenia/kernel/util/game_info_database.h @@ -0,0 +1,135 @@ +/** + ****************************************************************************** + * 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. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_UTIL_GAME_INFO_DATABASE_H_ +#define XENIA_KERNEL_UTIL_GAME_INFO_DATABASE_H_ + +#include +#include + +#include "xenia/kernel/util/xdbf_utils.h" +#include "xenia/kernel/util/xlast.h" + +namespace xe { +namespace kernel { +namespace util { + +class GameInfoDatabase { + public: + struct Context { + uint32_t id; + uint32_t max_value; + uint32_t default_value; + std::string description; + }; + + struct Property { + uint32_t id; + uint32_t data_size; + std::string description; + }; + + struct Achievement { + uint32_t id; + std::string label; + std::string description; + std::string unachieved_description; + uint32_t image_id; + uint32_t gamerscore; + uint32_t flags; + }; + + struct Query { + uint32_t id; + std::string name; + std::vector input_parameters; + std::vector filters; + std::vector expected_return; + }; + + struct Filter { + uint32_t left_id; + uint32_t right_id; + std::string comparation_operator; + }; + + struct Field { + uint32_t ordinal; + std::string name; + bool is_hidden; + uint16_t attribute_id; + // std::map + std::map property_aggregation; + }; + + struct StatsView { + uint32_t id; + std::string name; + std::vector fields; + }; + + struct ProductInformation { + uint32_t max_offline_players_count; + uint32_t max_systemlink_players_count; + uint32_t max_live_players_count; + std::string publisher_name; + std::string developer_name; + std::string marketing_info; + std::string genre_description; + std::vector features; + }; + + // 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(); + + bool IsValid() const { return is_valid_; } + + // This is mostly extracted from XDBF. + std::string GetTitleName( + const XLanguage language = XLanguage::kInvalid) const; + + XLanguage GetDefaultLanguage() const; + std::string GetLocalizedString( + const uint32_t id, XLanguage language = XLanguage::kInvalid) const; + + std::vector GetIcon() const; + + Context GetContext(const uint32_t id) const; + Property GetProperty(const uint32_t id) const; + Achievement GetAchievement(const uint32_t id) const; + + // TODO: Implement it in the future. + StatsView GetStatsView(const uint32_t id) const; + std::vector GetMatchmakingAttributes(const uint32_t id) const; + + // This is extracted from XLast. + Query GetQueryData(const uint32_t id) const; + std::vector GetSupportedLanguages() const; + ProductInformation GetProductInformation() const; + + // Aggregators for specific usecases + std::vector GetContexts() const; + std::vector GetProperties() const; + std::vector GetAchievements() const; + // TODO: Implement it in the future. + std::vector GetStatsViews() const; + + private: + bool is_valid_ = false; + std::unique_ptr xdbf_gamedata_; + std::unique_ptr xlast_gamedata_; +}; + +} // namespace util +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_UTIL_GAME_INFO_DATABASE_H_ \ No newline at end of file diff --git a/src/xenia/kernel/util/xlast.cc b/src/xenia/kernel/util/xlast.cc new file mode 100644 index 000000000..3bb784356 --- /dev/null +++ b/src/xenia/kernel/util/xlast.cc @@ -0,0 +1,248 @@ +/** + ****************************************************************************** + * 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/xlast.h" +#include "third_party/zlib/zlib.h" +#include "xenia/base/filesystem.h" +#include "xenia/base/logging.h" +#include "xenia/base/string_util.h" + +namespace xe { +namespace kernel { +namespace util { + +XLastMatchmakingQuery::XLastMatchmakingQuery() {} +XLastMatchmakingQuery::XLastMatchmakingQuery( + const pugi::xpath_node query_node) { + node_ = query_node; +} + +std::string XLastMatchmakingQuery::GetName() const { + return node_.node().attribute("friendlyName").value(); +} + +std::vector XLastMatchmakingQuery::GetReturns() const { + return XLast::GetAllValuesFromNode(node_, "Returns", "id"); +} + +std::vector XLastMatchmakingQuery::GetParameters() const { + return XLast::GetAllValuesFromNode(node_, "Parameters", "id"); +} + +std::vector XLastMatchmakingQuery::GetFilters() const { + return XLast::GetAllValuesFromNode(node_, "Filters", "left"); +} + +XLast::XLast() : parsed_xlast_(nullptr) {} + +XLast::XLast(const uint8_t* compressed_xml_data, + const uint32_t compressed_data_size, + const uint32_t decompressed_data_size) { + if (!compressed_data_size || !decompressed_data_size) { + XELOGW("XLast: Current title don't have any XLast XML data!"); + return; + } + + parsed_xlast_ = std::make_unique(); + xlast_decompressed_xml_.resize(decompressed_data_size); + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + + int ret = inflateInit2( + &stream, 16 + MAX_WBITS); // 16 + MAX_WBITS enables gzip decoding + if (ret != Z_OK) { + XELOGE("XLast: Error during Zlib stream init"); + return; + } + + stream.avail_in = compressed_data_size; + stream.next_in = + reinterpret_cast(const_cast(compressed_xml_data)); + stream.avail_out = decompressed_data_size; + stream.next_out = reinterpret_cast(xlast_decompressed_xml_.data()); + + ret = inflate(&stream, Z_NO_FLUSH); + if (ret == Z_STREAM_ERROR) { + XELOGE("XLast: Error during XLast decompression"); + inflateEnd(&stream); + return; + } + inflateEnd(&stream); + + parse_result_ = parsed_xlast_->load_buffer(xlast_decompressed_xml_.data(), + xlast_decompressed_xml_.size()); +} + +XLast::~XLast() {} + +std::u16string XLast::GetTitleName() const { + std::string xpath = "/XboxLiveSubmissionProject/GameConfigProject"; + + const pugi::xpath_node node = parsed_xlast_->select_node(xpath.c_str()); + if (!node) { + return std::u16string(); + } + + return xe::to_utf16(node.node().attribute("titleName").value()); +} + +std::map +XLast::GetProductInformationAttributes() const { + std::map attributes; + + std::string xpath = + "/XboxLiveSubmissionProject/GameConfigProject/ProductInformation"; + + const pugi::xpath_node node = parsed_xlast_->select_node(xpath.c_str()); + if (!node) { + return attributes; + } + + const auto node_attributes = node.node().attributes(); + for (const auto& attribute : node_attributes) { + const auto entry = + product_information_entry_string_to_enum.find(attribute.name()); + if (entry == product_information_entry_string_to_enum.cend()) { + XELOGW("GetProductInformationAttributes: Missing attribute: {}", + attribute.name()); + continue; + } + + std::string attribute_value = std::string(attribute.value()); + if (attribute_value.empty()) { + XELOGW( + "GetProductInformationAttributes: Attribute: {} Contains no value!", + attribute.name()); + continue; + } + + attributes.emplace(entry->second, + xe::string_util::from_string(attribute_value)); + } + + return attributes; +} + +std::vector XLast::GetSupportedLanguages() const { + std::vector launguages; + + std::string xpath = fmt::format( + "/XboxLiveSubmissionProject/GameConfigProject/LocalizedStrings"); + + const pugi::xpath_node node = parsed_xlast_->select_node(xpath.c_str()); + if (!node) { + return launguages; + } + + const auto locale = node.node().children("SupportedLocale"); + for (auto itr = locale.begin(); itr != locale.end(); itr++) { + const std::string locale_name = itr->attribute("locale").value(); + + for (const auto& language : language_mapping) { + if (language.second == locale_name) { + launguages.push_back(language.first); + } + } + } + + return launguages; +} + +std::u16string XLast::GetLocalizedString(uint32_t string_id, + XLanguage language) const { + std::string xpath = fmt::format( + "/XboxLiveSubmissionProject/GameConfigProject/LocalizedStrings/" + "LocalizedString[@id = \"{}\"]", + string_id); + + const pugi::xpath_node node = parsed_xlast_->select_node(xpath.c_str()); + if (!node) { + return std::u16string(); + } + + const std::string locale_name = GetLocaleStringFromLanguage(language); + const pugi::xml_node locale_node = + node.node().find_child_by_attribute("locale", locale_name.c_str()); + + if (!locale_node) { + return std::u16string(); + } + + return xe::to_utf16(locale_node.child_value()); +} + +XLastMatchmakingQuery* XLast::GetMatchmakingQuery( + const uint32_t query_id) const { + std::string xpath = fmt::format( + "/XboxLiveSubmissionProject/GameConfigProject/Matchmaking/Queries/" + "Query[@id = \"{}\"]", + query_id); + + XLastMatchmakingQuery* query = nullptr; + pugi::xpath_node node = parsed_xlast_->select_node(xpath.c_str()); + if (!node) { + return query; + } + + return new XLastMatchmakingQuery(node); +} + +std::vector XLast::GetAllValuesFromNode( + const pugi::xpath_node node, const std::string child_name, + const std::string attirbute_name) { + std::vector result{}; + + const auto searched_child = node.node().child(child_name.c_str()); + + for (pugi::xml_node_iterator itr = searched_child.begin(); + itr != searched_child.end(); itr++) { + result.push_back(xe::string_util::from_string( + itr->attribute(attirbute_name.c_str()).value(), true)); + } + + return result; +} + +void XLast::Dump(std::string file_name) const { + if (xlast_decompressed_xml_.empty()) { + return; + } + + if (file_name.empty()) { + file_name = xe::to_utf8(GetTitleName()); + } + + FILE* outfile = + xe::filesystem::OpenFile(fmt::format("{}.xml", file_name).c_str(), "ab"); + if (!outfile) { + return; + } + + fwrite(xlast_decompressed_xml_.data(), 1, xlast_decompressed_xml_.size(), + outfile); + fclose(outfile); +} + +std::string XLast::GetLocaleStringFromLanguage(XLanguage language) const { + const auto value = language_mapping.find(language); + if (value != language_mapping.cend()) { + return value->second; + } + + return language_mapping.at(XLanguage::kEnglish); +} + +} // namespace util +} // namespace kernel +} // namespace xe \ No newline at end of file diff --git a/src/xenia/kernel/util/xlast.h b/src/xenia/kernel/util/xlast.h new file mode 100644 index 000000000..45aaf8564 --- /dev/null +++ b/src/xenia/kernel/util/xlast.h @@ -0,0 +1,100 @@ +/** + ****************************************************************************** + * 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. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_UTIL_XLAST_H_ +#define XENIA_KERNEL_UTIL_XLAST_H_ + +#include +#include +#include + +#include "third_party/pugixml/src/pugixml.hpp" +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace util { + +enum class ProductInformationEntry { + MaxOfflinePlayers, + MaxSystemLinkPlayers, + MaxLivePlayers, + PublisherString, + DeveloperString, + MarketingString, + GenreTypeString +}; + +static const std::map + product_information_entry_string_to_enum = { + {"offlinePlayersMax", ProductInformationEntry::MaxOfflinePlayers}, + {"systemLinkPlayersMax", ProductInformationEntry::MaxSystemLinkPlayers}, + {"livePlayersMax", ProductInformationEntry::MaxLivePlayers}, + {"publisherStringId", ProductInformationEntry::PublisherString}, + {"developerStringId", ProductInformationEntry::DeveloperString}, + {"sellTextStringId", ProductInformationEntry::MarketingString}, + {"genreTextStringId", ProductInformationEntry::GenreTypeString}, +}; + +static const std::map language_mapping = { + {XLanguage::kEnglish, "en-US"}, {XLanguage::kJapanese, "ja-JP"}, + {XLanguage::kGerman, "de-DE"}, {XLanguage::kFrench, "fr-FR"}, + {XLanguage::kSpanish, "es-ES"}, {XLanguage::kItalian, "it-IT"}, + {XLanguage::kKorean, "ko-KR"}, {XLanguage::kTChinese, "zh-CHT"}, + {XLanguage::kPortuguese, "pt-PT"}, {XLanguage::kPolish, "pl-PL"}, + {XLanguage::kRussian, "ru-RU"}}; + +class XLastMatchmakingQuery { + public: + XLastMatchmakingQuery(); + XLastMatchmakingQuery(const pugi::xpath_node query_node); + + std::string GetName() const; + std::vector GetReturns() const; + std::vector GetParameters() const; + std::vector GetFilters() const; + + private: + pugi::xpath_node node_; +}; + +class XLast { + public: + XLast(); + XLast(const uint8_t* compressed_xml_data, const uint32_t compressed_data_size, + const uint32_t decompressed_data_size); + ~XLast(); + + std::u16string GetTitleName() const; + std::map GetProductInformationAttributes() + const; + + std::vector GetSupportedLanguages() const; + std::u16string GetLocalizedString(uint32_t string_id, + XLanguage language) const; + XLastMatchmakingQuery* GetMatchmakingQuery(uint32_t query_id) const; + static std::vector GetAllValuesFromNode( + const pugi::xpath_node node, const std::string child_name, + const std::string attirbute_name); + + void Dump(std::string file_name) const; + + private: + std::string GetLocaleStringFromLanguage(XLanguage language) const; + + std::vector xlast_decompressed_xml_; + std::unique_ptr parsed_xlast_ = nullptr; + pugi::xml_parse_result parse_result_ = {}; +}; + +} // namespace util +} // namespace kernel +} // namespace xe + +#endif diff --git a/src/xenia/kernel/util/xuserdata.h b/src/xenia/kernel/util/xuserdata.h index 348f06847..c2a6c502e 100644 --- a/src/xenia/kernel/util/xuserdata.h +++ b/src/xenia/kernel/util/xuserdata.h @@ -16,6 +16,16 @@ 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, diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 0d226ed2f..a8d684f1b 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -146,7 +146,7 @@ class UserSetting { X_USER_PROFILE_SETTING_SOURCE::DEFAULT; X_USER_PROFILE_SETTING_HEADER header_ = {}; - UserData::Key setting_id_ = {}; + AttributeKey setting_id_ = {}; std::unique_ptr user_data_ = nullptr; }; diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 55cb12d85..ebe7a9643 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -200,7 +200,7 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index, uint32_t needed_data_size = 0; for (uint32_t i = 0; i < setting_count; ++i) { needed_header_size += sizeof(X_USER_PROFILE_SETTING); - UserData::Key setting_key; + AttributeKey setting_key; setting_key.value = static_cast(setting_ids[i]); switch (static_cast(setting_key.type)) { case X_USER_DATA_TYPE::WSTRING: diff --git a/third_party/pugixml b/third_party/pugixml new file mode 160000 index 000000000..b2b466403 --- /dev/null +++ b/third_party/pugixml @@ -0,0 +1 @@ +Subproject commit b2b466403084667c90a0f0cc4e960405cfc8117a diff --git a/third_party/pugixml.lua b/third_party/pugixml.lua new file mode 100644 index 000000000..b2ae8f14b --- /dev/null +++ b/third_party/pugixml.lua @@ -0,0 +1,10 @@ +group("third_party") +project("pugixml") + uuid("d1089e5e-46ae-48c9-bee6-d38c674c8f61") + kind("StaticLib") + language("C++") + files({ + "pugixml/src/pugiconfig.hpp", + "pugixml/src/pugixml.cpp", + "pugixml/src/pugixml.hpp", + }) \ No newline at end of file diff --git a/third_party/zlib b/third_party/zlib new file mode 160000 index 000000000..51b7f2abd --- /dev/null +++ b/third_party/zlib @@ -0,0 +1 @@ +Subproject commit 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf diff --git a/third_party/zlib.lua b/third_party/zlib.lua new file mode 100644 index 000000000..2003eef00 --- /dev/null +++ b/third_party/zlib.lua @@ -0,0 +1,9 @@ +group("third_party") +project("zlib") + uuid("13d4073d-f1c9-47e3-a057-79b133596fc2") + kind("StaticLib") + language("C") + files({ + "zlib/*.c", + "zlib/*.h", + }) \ No newline at end of file