[XAM] Implemented SPA Stats Views

This commit is contained in:
Adrian
2025-11-02 18:54:42 +00:00
committed by Radosław Gliński
parent 4ebf8994de
commit 243178a310
7 changed files with 428 additions and 22 deletions

View File

@@ -75,6 +75,19 @@ using UserDataTypes = std::variant<uint32_t, int32_t, float, int64_t, double,
std::u16string, std::vector<uint8_t>>;
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:

View File

@@ -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<uint16_t>(SpaSection::kMetadata), kXdbfIdXvc2);
if (!stats_metadata) {
return;
}
auto xvc2_head =
reinterpret_cast<const XdbfSectionHeader*>(stats_metadata->data.data());
assert_true(xvc2_head->magic == kXdbfSignatureXvc2);
assert_true(xvc2_head->version == 1);
std::vector<SharedView> shared_views = {};
const uint16_t shared_view_metadata_count =
xe::load_and_swap<uint16_t>(xvc2_head + 1);
auto shared_view_meta_table_entry_ptr =
reinterpret_cast<const uint8_t*>(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<const SharedViewMetaTableEntry*>(
shared_view_meta_table_entry_ptr);
auto view_field_ptr =
reinterpret_cast<const ViewFieldEntry*>(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<const ViewFieldEntry*>(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<const ViewFieldEntry*>(
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<const XdbfSectionHeader*>(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<const PropertyBagEntry*>(xpbm_head + 1);
auto contexts_ptr =
reinterpret_cast<const xe::be<uint32_t>*>(property_bag_header_ptr + 1);
auto properties_ptr = reinterpret_cast<const xe::be<uint32_t>*>(
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<const uint8_t*>(xpbm_head) + xpbm_size;
shared_views.push_back(shared_view);
}
const uint16_t tables_count =
xe::load_and_swap<uint16_t>(shared_view_meta_table_entry_ptr);
const StatsViewTableEntry* stats_views_ptr =
reinterpret_cast<const StatsViewTableEntry*>(
shared_view_meta_table_entry_ptr + sizeof(uint16_t));
const auto views = std::vector<StatsViewTableEntry>(
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<uint32_t>(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<const XdbfPropertyTableEntry*>(properties_, id);
}
const std::optional<ViewTable> 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 <typename T>
T SpaInfo::GetSpaEntry(std::vector<T>& container, uint32_t id) {
for (const auto& entry : container) {

View File

@@ -14,6 +14,8 @@
#include <map>
#include <numeric>
#include <optional>
#include <set>
#include <string>
#include <vector>
@@ -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<AggregationType>(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<ViewType>(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<uint16_t>::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<uint32_t>(view_type));
}
constexpr inline ViewType GetViewType(const uint32_t flags) {
return static_cast<ViewType>(flags & kViewTypeMask);
}
constexpr inline bool IsArbitrated(const uint32_t flags) {
return flags & static_cast<uint32_t>(StatsViewFlags::kArbitrated);
}
constexpr inline bool IsHidden(const uint32_t flags) {
return flags & static_cast<uint32_t>(StatsViewFlags::kHidden);
}
constexpr inline bool IsTeamView(const uint32_t flags) {
return flags & static_cast<uint32_t>(StatsViewFlags::kTeamView);
}
constexpr inline bool IsOnlineOnly(const uint32_t flags) {
return flags & static_cast<uint32_t>(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<uint32_t> contexts_count;
xe::be<uint32_t> properties_count;
};
struct PropertyBag {
std::set<xe::be<uint32_t>> contexts;
std::set<xe::be<uint32_t>> properties;
};
struct TitleHeaderData {
xe::be<uint32_t> title_id;
xe::be<TitleType> title_type;
@@ -64,7 +181,7 @@ static_assert_size(TitleHeaderData, 32);
struct StatsViewTableEntry {
xe::be<uint32_t> id;
xe::be<uint32_t> flags;
xe::be<uint32_t> flags; // StatsViewFlags
xe::be<uint16_t> shared_index;
xe::be<uint16_t> string_id;
xe::be<uint32_t> unused;
@@ -74,10 +191,10 @@ static_assert_size(StatsViewTableEntry, 0x10);
struct ViewFieldEntry {
xe::be<uint32_t> size;
xe::be<uint32_t> property_id;
xe::be<uint32_t> flags;
xe::be<uint32_t> flags; // ViewFieldEntryFlags
xe::be<uint16_t> attribute_id;
xe::be<uint16_t> string_id;
xe::be<uint16_t> aggregation_type;
xe::be<uint16_t> aggregation_type; // AggregationType
xe::be<uint8_t> ordinal;
xe::be<uint8_t> field_type;
xe::be<uint32_t> format_type;
@@ -94,11 +211,6 @@ struct SharedViewMetaTableEntry {
};
static_assert_size(SharedViewMetaTableEntry, 0xC);
struct PropertyBag {
std::vector<xe::be<uint32_t>> contexts;
std::vector<xe::be<uint32_t>> properties;
};
struct SharedView {
std::vector<ViewFieldEntry> column_entries;
std::vector<ViewFieldEntry> row_entries;
@@ -176,8 +288,11 @@ class SpaInfo : public XdbfFile {
return properties_;
}
const std::vector<ViewTable>* GetStatsViews() const { return &stats_views_; }
const XdbfContextTableEntry* GetContext(uint32_t id);
const XdbfPropertyTableEntry* GetProperty(uint32_t id);
const std::optional<ViewTable> 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<const AchievementTableEntry*> achievements_;
std::vector<const XdbfContextTableEntry*> contexts_;
std::vector<const XdbfPropertyTableEntry*> properties_;
std::vector<ViewTable> stats_views_;
typedef std::map<uint16_t, std::string> XdbfLanguageStrings;
using XdbfLanguageStrings = std::map<uint16_t, std::string>;
std::map<XLanguage, XdbfLanguageStrings> language_strings_;
@@ -215,6 +331,8 @@ class SpaInfo : public XdbfFile {
void LoadContexts();
void LoadProperties();
void LoadStatsViews();
template <typename T>
static T GetSpaEntry(std::vector<T>& container, uint32_t id);
};

View File

@@ -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 {