diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 5125d9d72..8f3eb0149 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -7,6 +7,8 @@ ****************************************************************************** */ +#include + #include "xenia/emulator.h" #include "config.h" @@ -1538,7 +1540,7 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, entry.description, type, fmt::format("{}", entry.gamerscore)}); } - XELOGI("-------------------- ACHIEVEMENTS --------------------\n{}", + XELOGI("\n-------------------- ACHIEVEMENTS --------------------\n{}", table.str()); const std::vector @@ -1555,7 +1557,7 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, table.add_row({fmt::format("{:08X}", entry.id), label, fmt::format("{}", entry.data_size)}); } - XELOGI("-------------------- PROPERTIES --------------------\n{}", + XELOGI("\n-------------------- PROPERTIES --------------------\n{}", table.str()); const std::vector contexts_list = @@ -1573,9 +1575,46 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, fmt::format("{}", entry.default_value), fmt::format("{}", entry.max_value)}); } - XELOGI("-------------------- CONTEXTS --------------------\n{}", + XELOGI("\n-------------------- CONTEXTS --------------------\n{}", table.str()); + const std::vector stats_views = + game_info_database_->GetStatsViews(); + + // 4D5307EA SPA contains a lot of stats, limit views to log. + const auto stats_views_limit = stats_views | std::views::take(100); + + table = tabulate::Table(); + table.format().multi_byte_characters(true); + table.add_row({"ID", "View Type", "Name", "Skilled", "Arbitrated", + "Hidden", "Team View", "Online Only"}); + + for (const kernel::util::GameInfoDatabase::StatsView& entry : + stats_views_limit) { + const std::string name = + string_util::remove_eol(string_util::trim(entry.view.name)); + + const std::string view_type = + kernel::xam::GetViewTypeName(entry.view.view_type); + + table.add_row({fmt::format("{:08X}", entry.view.id), view_type, name, + entry.view.skilled ? "True" : "False", + entry.view.arbitrated ? "True" : "False", + entry.view.hidden ? "True" : "False", + entry.view.team_view ? "True" : "False", + entry.view.online_only ? "True" : "False"}); + } + + std::string totals; + + if (stats_views.size() > stats_views_limit.size()) { + totals = fmt::format("\nViews: {}/{}", stats_views_limit.size(), + stats_views.size()); + } + + XELOGI("\n-------------------- Stats Views --------------------{}\n{}", + totals.c_str(), table.str()); + 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/kernel/util/game_info_database.cc b/src/xenia/kernel/util/game_info_database.cc index 5b38540fe..5cde335db 100644 --- a/src/xenia/kernel/util/game_info_database.cc +++ b/src/xenia/kernel/util/game_info_database.cc @@ -149,6 +149,85 @@ GameInfoDatabase::Achievement GameInfoDatabase::GetAchievement( return achievement; } +GameInfoDatabase::PropertyBag GameInfoDatabase::GetPropertyBag( + const xam::PropertyBag& property_bag) const { + PropertyBag property_bag_native = {}; + + property_bag_native.contexts = {property_bag.contexts.cbegin(), + property_bag.contexts.cend()}; + property_bag_native.properties = {property_bag.properties.cbegin(), + property_bag.properties.cend()}; + + return property_bag_native; +} + +GameInfoDatabase::Field GameInfoDatabase::GetField( + const xam::ViewFieldEntry& field_entry) const { + Field field = {}; + + field.property_id = field_entry.property_id; + field.flags = field_entry.flags; + field.attribute_id = field_entry.attribute_id; + field.aggregation_type = field_entry.aggregation_type; + field.ordinal = field_entry.ordinal; + field.field_type = field_entry.field_type; + field.format_type = field_entry.format_type; + field.name = GetLocalizedString(field_entry.string_id); + + if (field.name.empty()) { + field.name = xam::AttributeIdToName(field.attribute_id); + } + + return field; +} + +GameInfoDatabase::StatsView GameInfoDatabase::GetStatsView( + const uint32_t id) const { + StatsView stats_view = {}; + + if (!is_valid_) { + return stats_view; + } + + const auto xdbf_stats_view = spa_gamedata_->GetStatsView(id); + + if (!xdbf_stats_view.has_value()) { + return stats_view; + } + + stats_view.view.id = xdbf_stats_view->view_entry.id; + + stats_view.view.arbitrated = + xam::IsArbitrated(xdbf_stats_view->view_entry.flags); + stats_view.view.hidden = xam::IsHidden(xdbf_stats_view->view_entry.flags); + stats_view.view.team_view = + xam::IsTeamView(xdbf_stats_view->view_entry.flags); + stats_view.view.online_only = + xam::IsOnlineOnly(xdbf_stats_view->view_entry.flags); + + stats_view.view.view_type = + xam::GetViewType(xdbf_stats_view->view_entry.flags); + stats_view.view.skilled = xam::IsLeaderboardIdSkill(stats_view.view.id); + + stats_view.view.shared_index = xdbf_stats_view->view_entry.shared_index; + + stats_view.view.name = + GetLocalizedString(xdbf_stats_view->view_entry.string_id); + + for (const auto& column : xdbf_stats_view->shared_view.column_entries) { + stats_view.shared_view.column_entries.push_back(GetField(column)); + } + + for (const auto& row : xdbf_stats_view->shared_view.row_entries) { + stats_view.shared_view.row_entries.push_back(GetField(row)); + } + + stats_view.shared_view.properties = + GetPropertyBag(xdbf_stats_view->shared_view.property_bag); + + return stats_view; +} + std::vector GameInfoDatabase::GetMatchmakingAttributes( const uint32_t id) const { // TODO(Gliniak): Implement when we will fully understand how to read it from @@ -279,6 +358,23 @@ std::vector GameInfoDatabase::GetAchievements() return achievements; } +std::vector GameInfoDatabase::GetStatsViews() + const { + std::vector stats_views; + + if (!is_valid_) { + return stats_views; + } + + const auto xdbf_stats_views = spa_gamedata_->GetStatsViews(); + + for (const auto& entry : *xdbf_stats_views) { + stats_views.push_back(GetStatsView(entry.view_entry.id)); + } + + return stats_views; +} + } // namespace util } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/util/game_info_database.h b/src/xenia/kernel/util/game_info_database.h index 485a52203..c7fcae5a9 100644 --- a/src/xenia/kernel/util/game_info_database.h +++ b/src/xenia/kernel/util/game_info_database.h @@ -10,6 +10,7 @@ #ifndef XENIA_KERNEL_UTIL_GAME_INFO_DATABASE_H_ #define XENIA_KERNEL_UTIL_GAME_INFO_DATABASE_H_ +#include #include #include @@ -22,6 +23,11 @@ namespace util { class GameInfoDatabase { public: + struct PropertyBag { + std::set contexts; + std::set properties; + }; + struct Context { uint32_t id; uint32_t max_value; @@ -60,18 +66,37 @@ class GameInfoDatabase { }; struct Field { - uint32_t ordinal; - std::string name; - bool is_hidden; + uint32_t property_id; + uint32_t flags; uint16_t attribute_id; - // std::map - std::map property_aggregation; + uint16_t aggregation_type; + uint8_t ordinal; + uint8_t field_type; + uint32_t format_type; + std::string name; + }; + + struct SharedView { + std::vector column_entries; + std::vector row_entries; + PropertyBag properties; + }; + + struct View { + uint32_t id; + bool arbitrated; + bool hidden; + bool team_view; + bool online_only; + bool skilled; + kernel::xam::ViewType view_type; + uint16_t shared_index; + std::string name; }; struct StatsView { - uint32_t id; - std::string name; - std::vector fields; + View view; + SharedView shared_view; }; struct ProductInformation { @@ -106,9 +131,11 @@ class GameInfoDatabase { Context GetContext(const uint32_t id) const; Property GetProperty(const uint32_t id) const; Achievement GetAchievement(const uint32_t id) const; + PropertyBag GetPropertyBag(const xam::PropertyBag& property_bag) const; + Field GetField(const xam::ViewFieldEntry& field_entry) const; + StatsView GetStatsView(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. @@ -120,7 +147,6 @@ class GameInfoDatabase { std::vector GetContexts() const; std::vector GetProperties() const; std::vector GetAchievements() const; - // TODO: Implement it in the future. std::vector GetStatsViews() const; private: diff --git a/src/xenia/kernel/xam/user_data.h b/src/xenia/kernel/xam/user_data.h index bc5b50acc..f6b73d03a 100644 --- a/src/xenia/kernel/xam/user_data.h +++ b/src/xenia/kernel/xam/user_data.h @@ -75,6 +75,19 @@ using UserDataTypes = std::variant>; constexpr uint32_t kMaxUserDataSize = 0x03E8; +constexpr uint32_t kInvalidSize = 0xFFFF; + +constexpr uint32_t kPropertyScopeMask = 0x8000; +constexpr uint32_t kPropertyIdMask = 0x7FFF; +constexpr uint32_t kPropertyTypeMask = 0xF0000000; + +constexpr uint32_t kInvalidPropertyId = 0xFFFF; + +constexpr uint32_t kInvalidContextId = 0xFFFF; +constexpr uint32_t kMaxContextId = 0x7FFF; + +constexpr uint32_t kInvalidContextValue = 0xFFFF; +constexpr uint32_t kMaxContextValue = 0x7FFF; class UserData { public: diff --git a/src/xenia/kernel/xam/xdbf/spa_info.cc b/src/xenia/kernel/xam/xdbf/spa_info.cc index f9c42c3fe..bb158a87d 100644 --- a/src/xenia/kernel/xam/xdbf/spa_info.cc +++ b/src/xenia/kernel/xam/xdbf/spa_info.cc @@ -24,6 +24,7 @@ void SpaInfo::Load() { LoadAchievements(); LoadProperties(); LoadContexts(); + LoadStatsViews(); } bool operator<(const SpaInfo& first, const SpaInfo& second) { @@ -148,6 +149,109 @@ void SpaInfo::LoadContexts() { } } +void SpaInfo::LoadStatsViews() { + auto stats_metadata = + GetEntry(static_cast(SpaSection::kMetadata), kXdbfIdXvc2); + if (!stats_metadata) { + return; + } + + auto xvc2_head = + reinterpret_cast(stats_metadata->data.data()); + assert_true(xvc2_head->magic == kXdbfSignatureXvc2); + assert_true(xvc2_head->version == 1); + + std::vector shared_views = {}; + + const uint16_t shared_view_metadata_count = + xe::load_and_swap(xvc2_head + 1); + + auto shared_view_meta_table_entry_ptr = + reinterpret_cast(xvc2_head + 1); + + shared_view_meta_table_entry_ptr += sizeof(uint16_t); + + for (uint32_t i = 0; i < shared_view_metadata_count; i++) { + SharedView shared_view = {}; + + auto shared_view_entry_ptr = + reinterpret_cast( + shared_view_meta_table_entry_ptr); + + auto view_field_ptr = + reinterpret_cast(shared_view_entry_ptr + 1); + + for (uint32_t i = 0; i < shared_view_entry_ptr->column_count; i++) { + shared_view.column_entries.push_back( + *reinterpret_cast(view_field_ptr + i)); + } + + for (uint32_t i = 0; i < shared_view_entry_ptr->row_count; i++) { + shared_view.row_entries.push_back( + *reinterpret_cast( + view_field_ptr + shared_view_entry_ptr->column_count + i)); + } + + const uint32_t entries_count = + shared_view_entry_ptr->column_count + shared_view_entry_ptr->row_count; + + auto xpbm_head = reinterpret_cast(view_field_ptr + + entries_count); + + assert_true(xpbm_head->magic == kXdbfSignatureXpbm); + assert_true(xpbm_head->version == 1); + + const PropertyBagEntry* property_bag_header_ptr = + reinterpret_cast(xpbm_head + 1); + + auto contexts_ptr = + reinterpret_cast*>(property_bag_header_ptr + 1); + + auto properties_ptr = reinterpret_cast*>( + contexts_ptr + property_bag_header_ptr->contexts_count); + + for (uint32_t i = 0; i < property_bag_header_ptr->contexts_count; i++) { + shared_view.property_bag.contexts.insert(contexts_ptr[i]); + } + + for (uint32_t i = 0; i < property_bag_header_ptr->properties_count; i++) { + shared_view.property_bag.properties.insert(properties_ptr[i]); + } + + const uint32_t xpbm_size = xpbm_head->size + sizeof(uint32_t); + + shared_view_meta_table_entry_ptr = + reinterpret_cast(xpbm_head) + xpbm_size; + + shared_views.push_back(shared_view); + } + + const uint16_t tables_count = + xe::load_and_swap(shared_view_meta_table_entry_ptr); + + const StatsViewTableEntry* stats_views_ptr = + reinterpret_cast( + shared_view_meta_table_entry_ptr + sizeof(uint16_t)); + + const auto views = std::vector( + stats_views_ptr, stats_views_ptr + tables_count); + + for (const auto& view : views) { + ViewTable view_table = {}; + + // What are kContextByProperty and kContextByContext views? + assert_zero(static_cast(GetViewType(view.flags))); + + if (view.shared_index < shared_views.size()) { + view_table.shared_view = shared_views.at(view.shared_index); + } + + view_table.view_entry = view; + + stats_views_.push_back(view_table); + } +} + const uint8_t* SpaInfo::ReadXLast(uint32_t& compressed_size, uint32_t& decompressed_size) { auto xlast_table = @@ -276,6 +380,14 @@ const XdbfPropertyTableEntry* SpaInfo::GetProperty(uint32_t id) { return GetSpaEntry(properties_, id); } +const std::optional SpaInfo::GetStatsView(uint32_t id) { + const auto itr = std::ranges::find_if( + stats_views_, + [id](const ViewTable& view) { return view.view_entry.id == id; }); + + return itr != stats_views_.cend() ? std::make_optional(*itr) : std::nullopt; +} + template T SpaInfo::GetSpaEntry(std::vector& container, uint32_t id) { for (const auto& entry : container) { diff --git a/src/xenia/kernel/xam/xdbf/spa_info.h b/src/xenia/kernel/xam/xdbf/spa_info.h index eec35f3e3..8338dd17e 100644 --- a/src/xenia/kernel/xam/xdbf/spa_info.h +++ b/src/xenia/kernel/xam/xdbf/spa_info.h @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -47,7 +49,122 @@ enum class TitleFlags { kNeverIncludeInProfile = 2, }; +constexpr uint32_t kViewTypeMask = 0xF; + +enum class ViewType : uint32_t { + kLeaderboard = 0, + kContextByProperty = 1, + kContextByContext = 2 +}; + +enum class StatsViewFlags : uint16_t { + kArbitrated = 16, + kHidden = 32, + kTeamView = 64, + kOnlineOnly = 128, +}; + +enum class AggregationType : uint16_t { + kLast = 0x8001, + kMax = 0x800B, + kSum = 0x8003, + kMin = 0x8009, +}; + +enum class ViewFieldEntryFlags : uint32_t { + kHidden = 1, +}; + +enum class ViewFieldType : uint8_t { kContextField, kPropertyField }; + +constexpr inline std::string GetAggregationTypeName( + const uint32_t aggregation_type) { + switch (static_cast(aggregation_type)) { + case AggregationType::kLast: + return "Last"; + case AggregationType::kMax: + return "Max"; + case AggregationType::kSum: + return "Sum"; + case AggregationType::kMin: + return "Min"; + default: + return ""; + } +} + +constexpr inline std::string GetViewTypeName(const uint32_t view_type) { + switch (static_cast(view_type)) { + case ViewType::kLeaderboard: + return "Leaderboard"; + case ViewType::kContextByProperty: + return "Context by Property"; + case ViewType::kContextByContext: + return "Context by Context"; + default: + return ""; + } +} +constexpr inline std::string AttributeIdToName(const uint16_t id) { + switch (id) { + case std::numeric_limits::max(): + return "Rank"; + case 65534: + return "Rating"; + case 65533: + return "Gamertag"; + case 65530: + return "Attachment Size"; + default: + return ""; + } +} + +constexpr inline std::string GetViewTypeName(const ViewType view_type) { + return GetViewTypeName(static_cast(view_type)); +} + +constexpr inline ViewType GetViewType(const uint32_t flags) { + return static_cast(flags & kViewTypeMask); +} + +constexpr inline bool IsArbitrated(const uint32_t flags) { + return flags & static_cast(StatsViewFlags::kArbitrated); +} + +constexpr inline bool IsHidden(const uint32_t flags) { + return flags & static_cast(StatsViewFlags::kHidden); +} + +constexpr inline bool IsTeamView(const uint32_t flags) { + return flags & static_cast(StatsViewFlags::kTeamView); +} + +constexpr inline bool IsOnlineOnly(const uint32_t flags) { + return flags & static_cast(StatsViewFlags::kOnlineOnly); +} + +constexpr inline uint32_t GetSkillLeaderboardId(uint32_t game_type, + uint32_t game_mode) { + return (0xFFF00000 | (game_type == 1 ? 0xF0000 : 0xE0000)) | + (game_mode & 0xFFFF); +} + +constexpr inline bool IsLeaderboardIdSkill(uint32_t id) { + return (id & 0x2000000) != 0; +} + #pragma pack(push, 1) +struct PropertyBagEntry { + xe::be contexts_count; + xe::be properties_count; +}; + +struct PropertyBag { + std::set> contexts; + std::set> properties; +}; + struct TitleHeaderData { xe::be title_id; xe::be title_type; @@ -64,7 +181,7 @@ static_assert_size(TitleHeaderData, 32); struct StatsViewTableEntry { xe::be id; - xe::be flags; + xe::be flags; // StatsViewFlags xe::be shared_index; xe::be string_id; xe::be unused; @@ -74,10 +191,10 @@ static_assert_size(StatsViewTableEntry, 0x10); struct ViewFieldEntry { xe::be size; xe::be property_id; - xe::be flags; + xe::be flags; // ViewFieldEntryFlags xe::be attribute_id; xe::be string_id; - xe::be aggregation_type; + xe::be aggregation_type; // AggregationType xe::be ordinal; xe::be field_type; xe::be format_type; @@ -94,11 +211,6 @@ struct SharedViewMetaTableEntry { }; static_assert_size(SharedViewMetaTableEntry, 0xC); -struct PropertyBag { - std::vector> contexts; - std::vector> properties; -}; - struct SharedView { std::vector column_entries; std::vector row_entries; @@ -176,8 +288,11 @@ class SpaInfo : public XdbfFile { return properties_; } + const std::vector* GetStatsViews() const { return &stats_views_; } + const XdbfContextTableEntry* GetContext(uint32_t id); const XdbfPropertyTableEntry* GetProperty(uint32_t id); + const std::optional GetStatsView(uint32_t id); uint32_t total_gamerscore() const { return std::accumulate(achievements_.cbegin(), achievements_.cend(), 0, @@ -202,8 +317,9 @@ class SpaInfo : public XdbfFile { std::vector achievements_; std::vector contexts_; std::vector properties_; + std::vector stats_views_; - typedef std::map XdbfLanguageStrings; + using XdbfLanguageStrings = std::map; std::map language_strings_; @@ -215,6 +331,8 @@ class SpaInfo : public XdbfFile { void LoadContexts(); void LoadProperties(); + void LoadStatsViews(); + template static T GetSpaEntry(std::vector& container, uint32_t id); }; diff --git a/src/xenia/kernel/xam/xdbf/xdbf_io.h b/src/xenia/kernel/xam/xdbf/xdbf_io.h index 612d23ab1..88f879f44 100644 --- a/src/xenia/kernel/xam/xdbf/xdbf_io.h +++ b/src/xenia/kernel/xam/xdbf/xdbf_io.h @@ -34,6 +34,7 @@ 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 fourcc_t kXdbfSignatureXpbm = make_fourcc("XPBM"); constexpr uint64_t kXdbfIdTitle = 0x8000; constexpr uint64_t kXdbfIdXstc = 0x58535443; @@ -44,6 +45,7 @@ constexpr uint64_t kXdbfIdXvc2 = 0x58564332; constexpr uint64_t kXdbfIdXmat = 0x584D4154; constexpr uint64_t kXdbfIdXsrc = 0x58535243; constexpr uint64_t kXdbfIdXthd = 0x58544844; +constexpr uint64_t kXdbfIdXpbm = 0x5850424D; #pragma pack(push, 1) struct XdbfHeader {