[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
This commit is contained in:
Gliniak 2024-12-15 13:58:08 +01:00
parent b3d345610a
commit a7c7a3711d
43 changed files with 3709 additions and 1496 deletions

View File

@ -1794,10 +1794,11 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
}
if (!notificationTitle.empty()) {
app_context_.CallInUIThread([=]() {
new xe::ui::HostNotificationWindow(imgui_drawer(), notificationTitle,
notificationDesc, 0);
});
app_context_.CallInUIThread(
[imgui_drawer = imgui_drawer(), notificationTitle, notificationDesc]() {
new xe::ui::HostNotificationWindow(imgui_drawer, notificationTitle,
notificationDesc, 0);
});
}
xe::threading::Sleep(delay);

View File

@ -16,6 +16,7 @@
#include <cstring>
#include <regex>
#include <string>
#include <variant>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/assert.h"
@ -428,6 +429,28 @@ inline vec128_t from_string<vec128_t>(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<std::string, std::u16string> string,
bool include_terminator = true) {
if (std::holds_alternative<std::string>(string)) {
return std::get<std::string>(string).size() + include_terminator;
} else if (std::holds_alternative<std::u16string>(string)) {
return (std::get<std::u16string>(string).size() + include_terminator) *
sizeof(char16_t);
} else {
assert_always();
}
return 0;
}
} // namespace string_util
} // namespace xe

View File

@ -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<kernel::util::GameInfoDatabase>(&db);
game_info_database_ =
std::make_unique<kernel::util::GameInfoDatabase>(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);
}
}
}
}

View File

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

View File

@ -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<xam::SpaInfo> KernelState::title_xdbf() const {
return module_xdbf(executable_module_);
}
util::XdbfGameData KernelState::module_xdbf(
const std::unique_ptr<xam::SpaInfo> KernelState::module_xdbf(
object_ref<UserModule> 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<xam::SpaInfo>(std::span<uint8_t>(
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<uint8_t> 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<uint8_t>(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()); }

View File

@ -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<UserModule> exec_module) const;
const std::unique_ptr<xam::SpaInfo> title_xdbf() const;
const std::unique_ptr<xam::SpaInfo> module_xdbf(
object_ref<UserModule> exec_module) const;
bool UpdateSpaData(vfs::Entry* spa_file_update);
xam::XamState* xam_state() const { return xam_state_.get(); }

View File

@ -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<xam::SpaInfo>(*data);
spa_gamedata_->Load();
is_valid_ = true;
xdbf_gamedata_ = std::make_unique<XdbfGameData>(*data);
uint32_t compressed_size, decompressed_size = 0;
const uint8_t* xlast_ptr =
xdbf_gamedata_->ReadXLast(compressed_size, decompressed_size);
spa_gamedata_->ReadXLast(compressed_size, decompressed_size);
if (!xlast_ptr) {
XELOGW(
@ -48,26 +51,25 @@ GameInfoDatabase::GameInfoDatabase(const XdbfGameData* data) {
}
}
GameInfoDatabase::~GameInfoDatabase() {}
std::string GameInfoDatabase::GetTitleName(const XLanguage language) const {
if (!is_valid_) {
return "";
void GameInfoDatabase::Update(const xam::SpaInfo* new_spa) {
if (*spa_gamedata_ <= *new_spa) {
return;
}
return xdbf_gamedata_->title(xdbf_gamedata_->GetExistingLanguage(language));
Init(new_spa);
}
std::string GameInfoDatabase::GetTitleName(const XLanguage language) const {
return spa_gamedata_->title_name(
spa_gamedata_->GetExistingLanguage(language));
}
std::vector<uint8_t> GameInfoDatabase::GetIcon() const {
std::vector<uint8_t> data;
if (!is_valid_) {
return data;
}
const XdbfBlock icon = xdbf_gamedata_->icon();
data.resize(icon.size);
std::memcpy(data.data(), icon.buffer, icon.size);
const auto icon = spa_gamedata_->title_icon();
data.resize(icon.size());
std::memcpy(data.data(), icon.data(), icon.size());
return data;
}
@ -76,17 +78,13 @@ XLanguage GameInfoDatabase::GetDefaultLanguage() const {
return XLanguage::kEnglish;
}
return xdbf_gamedata_->default_language();
return spa_gamedata_->default_language();
}
std::string GameInfoDatabase::GetLocalizedString(const uint32_t id,
XLanguage language) const {
if (!is_valid_) {
return "";
}
return xdbf_gamedata_->GetStringTableEntry(
xdbf_gamedata_->GetExistingLanguage(language), id);
return spa_gamedata_->GetStringTableEntry(
spa_gamedata_->GetExistingLanguage(language), id);
}
GameInfoDatabase::Context GameInfoDatabase::GetContext(
@ -97,12 +95,15 @@ GameInfoDatabase::Context GameInfoDatabase::GetContext(
return context;
}
const auto xdbf_context = xdbf_gamedata_->GetContext(id);
const auto xdbf_context = spa_gamedata_->GetContext(id);
if (!xdbf_context) {
return context;
}
context.id = xdbf_context.id;
context.default_value = xdbf_context.default_value;
context.max_value = xdbf_context.max_value;
context.description = GetLocalizedString(xdbf_context.string_id);
context.id = xdbf_context->id;
context.default_value = xdbf_context->default_value;
context.max_value = xdbf_context->max_value;
context.description = GetLocalizedString(xdbf_context->string_id);
return context;
}
@ -114,11 +115,14 @@ GameInfoDatabase::Property GameInfoDatabase::GetProperty(
return property;
}
const auto xdbf_property = xdbf_gamedata_->GetProperty(id);
const auto xdbf_property = spa_gamedata_->GetProperty(id);
if (!xdbf_property) {
return property;
}
property.id = xdbf_property.id;
property.data_size = xdbf_property.data_size;
property.description = GetLocalizedString(xdbf_property.string_id);
property.id = xdbf_property->id;
property.data_size = xdbf_property->data_size;
property.description = GetLocalizedString(xdbf_property->string_id);
return property;
}
@ -130,17 +134,21 @@ GameInfoDatabase::Achievement GameInfoDatabase::GetAchievement(
return achievement;
}
const auto xdbf_achievement = xdbf_gamedata_->GetAchievement(id);
const auto xdbf_achievement = spa_gamedata_->GetAchievement(id);
if (!xdbf_achievement) {
return achievement;
}
achievement.id = xdbf_achievement.id;
achievement.image_id = xdbf_achievement.id;
achievement.gamerscore = xdbf_achievement.gamerscore;
achievement.flags = xdbf_achievement.flags;
achievement.id = xdbf_achievement->id;
achievement.image_id = xdbf_achievement->id;
achievement.gamerscore = xdbf_achievement->gamerscore;
achievement.flags = xdbf_achievement->flags;
achievement.label = GetLocalizedString(xdbf_achievement.label_id);
achievement.description = GetLocalizedString(xdbf_achievement.description_id);
achievement.label = GetLocalizedString(xdbf_achievement->label_id);
achievement.description =
GetLocalizedString(xdbf_achievement->description_id);
achievement.unachieved_description =
GetLocalizedString(xdbf_achievement.unachieved_id);
GetLocalizedString(xdbf_achievement->unachieved_id);
return achievement;
}
@ -148,13 +156,14 @@ std::vector<uint32_t> GameInfoDatabase::GetMatchmakingAttributes(
const uint32_t id) const {
std::vector<uint32_t> result;
const auto xdbf_matchmaking_data = xdbf_gamedata_->GetMatchCollection();
/*
const auto xdbf_matchmaking_data = spa_gamedata_->GetMatchCollection();
result.insert(result.end(), xdbf_matchmaking_data.contexts.cbegin(),
xdbf_matchmaking_data.contexts.cend());
result.insert(result.end(), xdbf_matchmaking_data.properties.cbegin(),
xdbf_matchmaking_data.properties.cend());
*/
return result;
}
@ -240,9 +249,9 @@ std::vector<GameInfoDatabase::Context> GameInfoDatabase::GetContexts() const {
return contexts;
}
const auto xdbf_contexts = xdbf_gamedata_->GetContexts();
const auto xdbf_contexts = spa_gamedata_->GetContexts();
for (const auto& entry : xdbf_contexts) {
contexts.push_back(GetContext(entry.id));
contexts.push_back(GetContext(entry->id));
}
return contexts;
@ -256,9 +265,9 @@ std::vector<GameInfoDatabase::Property> GameInfoDatabase::GetProperties()
return properties;
}
const auto xdbf_properties = xdbf_gamedata_->GetProperties();
const auto xdbf_properties = spa_gamedata_->GetProperties();
for (const auto& entry : xdbf_properties) {
properties.push_back(GetProperty(entry.id));
properties.push_back(GetProperty(entry->id));
}
return properties;
@ -272,9 +281,9 @@ std::vector<GameInfoDatabase::Achievement> 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;

View File

@ -13,8 +13,8 @@
#include <string>
#include <vector>
#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<StatsView> GetStatsViews() const;
private:
void Init(const xam::SpaInfo* data);
bool is_valid_ = false;
std::unique_ptr<XdbfGameData> xdbf_gamedata_;
std::unique_ptr<xam::SpaInfo> spa_gamedata_;
std::unique_ptr<XLast> xlast_gamedata_;
};

View File

@ -70,33 +70,34 @@ void Property::Write(Memory* memory, XUSER_PROPERTY* property) const {
switch (data_type_) {
case X_USER_DATA_TYPE::WSTRING:
property->data.binary.size = value_size_;
property->data.data.binary.size = value_size_;
break;
case X_USER_DATA_TYPE::CONTENT:
case X_USER_DATA_TYPE::BINARY:
property->data.binary.size = value_size_;
property->data.data.binary.size = value_size_;
// Property pointer must be valid at this point!
memcpy(memory->TranslateVirtual(property->data.binary.ptr), value_.data(),
value_size_);
memcpy(memory->TranslateVirtual(property->data.data.binary.ptr),
value_.data(), value_size_);
break;
case X_USER_DATA_TYPE::CONTEXT:
case X_USER_DATA_TYPE::INT32:
memcpy(reinterpret_cast<uint8_t*>(&property->data.s32), value_.data(),
value_size_);
memcpy(reinterpret_cast<uint8_t*>(&property->data.data.s32),
value_.data(), value_size_);
break;
case X_USER_DATA_TYPE::INT64:
memcpy(reinterpret_cast<uint8_t*>(&property->data.s64), value_.data(),
value_size_);
memcpy(reinterpret_cast<uint8_t*>(&property->data.data.s64),
value_.data(), value_size_);
break;
case X_USER_DATA_TYPE::DOUBLE:
memcpy(reinterpret_cast<uint8_t*>(&property->data.f64), value_.data(),
value_size_);
memcpy(reinterpret_cast<uint8_t*>(&property->data.data.f64),
value_.data(), value_size_);
break;
case X_USER_DATA_TYPE::FLOAT:
memcpy(reinterpret_cast<uint8_t*>(&property->data.f32), value_.data(),
value_size_);
memcpy(reinterpret_cast<uint8_t*>(&property->data.data.f32),
value_.data(), value_size_);
break;
case X_USER_DATA_TYPE::DATETIME:
memcpy(reinterpret_cast<uint8_t*>(&property->data.filetime),
memcpy(reinterpret_cast<uint8_t*>(&property->data.data.filetime),
value_.data(), value_size_);
break;
default:
@ -106,9 +107,9 @@ void Property::Write(Memory* memory, XUSER_PROPERTY* property) const {
userDataVariant Property::GetValue() const {
switch (data_type_) {
case X_USER_DATA_TYPE::CONTENT:
case X_USER_DATA_TYPE::BINARY:
return value_;
case X_USER_DATA_TYPE::CONTEXT:
case X_USER_DATA_TYPE::INT32:
return *reinterpret_cast<const uint32_t*>(value_.data());
case X_USER_DATA_TYPE::INT64:

View File

@ -47,8 +47,6 @@ class Property {
bool RequiresPointer() const {
return static_cast<X_USER_DATA_TYPE>(property_id_.type) ==
X_USER_DATA_TYPE::CONTENT ||
static_cast<X_USER_DATA_TYPE>(property_id_.type) ==
X_USER_DATA_TYPE::WSTRING ||
static_cast<X_USER_DATA_TYPE>(property_id_.type) ==
X_USER_DATA_TYPE::BINARY;

View File

@ -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 <map>
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<const XbdfHeader*>(ptr);
ptr += sizeof(XbdfHeader);
if (header_->magic != kXdbfSignatureXdbf) {
data_ = nullptr;
return;
}
entries_ = reinterpret_cast<const XbdfEntry*>(ptr);
ptr += sizeof(XbdfEntry) * header_->entry_count;
files_ = reinterpret_cast<const XbdfFileLoc*>(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<uint16_t>(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<uint64_t>(language));
if (!language_block) {
return "";
}
auto xstr_head =
reinterpret_cast<const XdbfSectionHeader*>(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>(*(uint16_t*)ptr);
ptr += sizeof(uint16_t);
for (uint16_t i = 0; i < string_count; ++i) {
auto entry = reinterpret_cast<const XdbfStringTableEntry*>(ptr);
ptr += sizeof(XdbfStringTableEntry);
if (entry->id == string_id) {
return std::string(reinterpret_cast<const char*>(ptr),
entry->string_length);
}
ptr += entry->string_length;
}
return "";
}
std::vector<XdbfAchievementTableEntry> XdbfWrapper::GetAchievements() const {
std::vector<XdbfAchievementTableEntry> achievements;
auto achievement_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXach);
if (!achievement_table) {
return achievements;
}
auto xach_head =
reinterpret_cast<const XdbfSectionHeader*>(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>(*(uint16_t*)ptr);
ptr += sizeof(uint16_t);
for (uint16_t i = 0; i < achievement_count; ++i) {
auto entry = reinterpret_cast<const XdbfAchievementTableEntry*>(ptr);
ptr += sizeof(XdbfAchievementTableEntry);
achievements.push_back(*entry);
}
return achievements;
}
std::vector<XdbfPropertyTableEntry> XdbfWrapper::GetProperties() const {
std::vector<XdbfPropertyTableEntry> properties;
auto property_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXprp);
if (!property_table) {
return properties;
}
auto xprp_head =
reinterpret_cast<const XdbfSectionHeader*>(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>(*(uint16_t*)ptr);
ptr += sizeof(uint16_t);
for (uint16_t i = 0; i < properties_count; i++) {
auto entry = reinterpret_cast<const XdbfPropertyTableEntry*>(ptr);
ptr += sizeof(XdbfPropertyTableEntry);
properties.push_back(*entry);
}
return properties;
}
std::vector<XdbfContextTableEntry> XdbfWrapper::GetContexts() const {
std::vector<XdbfContextTableEntry> contexts;
auto contexts_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXctx);
if (!contexts_table) {
return contexts;
}
auto xcxt_head =
reinterpret_cast<const XdbfSectionHeader*>(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>(*(uint32_t*)ptr);
ptr += sizeof(uint32_t);
for (uint32_t i = 0; i < contexts_count; i++) {
auto entry = reinterpret_cast<const XdbfContextTableEntry*>(ptr);
ptr += sizeof(XdbfContextTableEntry);
contexts.push_back(*entry);
}
return contexts;
}
std::vector<XdbfViewTable> XdbfWrapper::GetStatsView() const {
std::vector<XdbfViewTable> entries;
auto stats_table = GetEntry(XdbfSection::kMetadata, kXdbfIdXvc2);
if (!stats_table) {
return entries;
}
auto xvc2_head =
reinterpret_cast<const XdbfSectionHeader*>(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>(*(uint16_t*)ptr);
ptr += sizeof(uint16_t);
std::map<uint16_t, XdbfSharedView> 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<const XdbfStatsViewTableEntry*>(
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<const XdbfSectionHeader*>(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<const XdbfSectionHeader*>(xlast_table.buffer);
assert_true(xlast_head->magic == kXdbfSignatureXthd);
assert_true(xlast_head->version == 1);
const XdbfTitleHeaderData* ptr = reinterpret_cast<const XdbfTitleHeaderData*>(
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<const XdbfSharedViewMetaTableEntry*>(ptr);
ptr += sizeof(XdbfSharedViewMetaTableEntry);
for (uint16_t i = 0; i < table_header->column_count - 1; i++) {
auto view_field = reinterpret_cast<const XdbfViewFieldEntry*>(
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<const XdbfViewFieldEntry*>(
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<xe::be<uint32_t>> 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<xe::be<uint32_t>>& contexts,
std::vector<xe::be<uint32_t>>& properties) const {
auto xpbm_header = reinterpret_cast<const XdbfSectionHeader*>(ptr);
ptr += sizeof(XdbfSectionHeader);
byte_count += sizeof(XdbfSectionHeader) + 2 * sizeof(uint32_t);
uint32_t context_count = xe::byte_swap<uint32_t>(*(uint32_t*)ptr);
ptr += sizeof(uint32_t);
uint32_t properties_count = xe::byte_swap<uint32_t>(*(uint32_t*)ptr);
ptr += sizeof(uint32_t);
contexts = std::vector<xe::be<uint32_t>>(context_count);
std::memcpy(contexts.data(), ptr, context_count * sizeof(uint32_t));
ptr += context_count * sizeof(uint32_t);
properties = std::vector<xe::be<uint32_t>>(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<const XdbfSectionHeader*>(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<xe::be<uint32_t>> 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<const XdbfXstc*>(block.buffer);
assert_true(xstc->magic == kXdbfSignatureXstc);
return static_cast<XLanguage>(static_cast<uint32_t>(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

View File

@ -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 <string>
#include <vector>
#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<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> entry_count;
xe::be<uint32_t> entry_used;
xe::be<uint32_t> free_count;
xe::be<uint32_t> free_used;
};
static_assert_size(XbdfHeader, 24);
struct XbdfEntry {
xe::be<uint16_t> section;
xe::be<uint64_t> id;
xe::be<uint32_t> offset;
xe::be<uint32_t> size;
};
static_assert_size(XbdfEntry, 18);
struct XbdfFileLoc {
xe::be<uint32_t> offset;
xe::be<uint32_t> size;
};
static_assert_size(XbdfFileLoc, 8);
struct XdbfXstc {
xe::be<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> size;
xe::be<uint32_t> default_language;
};
static_assert_size(XdbfXstc, 16);
struct XdbfSectionHeader {
xe::be<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> size;
};
static_assert_size(XdbfSectionHeader, 12);
struct XdbfStringTableEntry {
xe::be<uint16_t> id;
xe::be<uint16_t> string_length;
};
static_assert_size(XdbfStringTableEntry, 4);
struct XdbfTitleHeaderData {
xe::be<uint32_t> title_id;
xe::be<uint32_t> title_type;
xe::be<uint16_t> major;
xe::be<uint16_t> minor;
xe::be<uint16_t> build;
xe::be<uint16_t> revision;
xe::be<uint32_t> padding_0;
xe::be<uint32_t> padding_1;
xe::be<uint32_t> padding_2;
xe::be<uint32_t> padding_3;
};
static_assert_size(XdbfTitleHeaderData, 32);
struct XdbfContextTableEntry {
xe::be<uint32_t> id;
xe::be<uint16_t> unk1;
xe::be<uint16_t> string_id;
xe::be<uint32_t> max_value;
xe::be<uint32_t> default_value;
};
static_assert_size(XdbfContextTableEntry, 16);
struct XdbfPropertyTableEntry {
xe::be<uint32_t> id;
xe::be<uint16_t> string_id;
xe::be<uint16_t> data_size;
};
static_assert_size(XdbfPropertyTableEntry, 8);
struct XdbfAchievementTableEntry {
xe::be<uint16_t> id;
xe::be<uint16_t> label_id;
xe::be<uint16_t> description_id;
xe::be<uint16_t> unachieved_id;
xe::be<uint32_t> image_id;
xe::be<uint16_t> gamerscore;
xe::be<uint16_t> unkE;
xe::be<uint32_t> flags;
xe::be<uint32_t> unk14;
xe::be<uint32_t> unk18;
xe::be<uint32_t> unk1C;
xe::be<uint32_t> unk20;
};
static_assert_size(XdbfAchievementTableEntry, 0x24);
struct XdbfStatsViewTableEntry {
xe::be<uint32_t> id;
xe::be<uint32_t> flags;
xe::be<uint16_t> shared_index;
xe::be<uint16_t> string_id;
xe::be<uint32_t> unused;
};
static_assert_size(XdbfStatsViewTableEntry, 0x10);
struct XdbfViewFieldEntry {
xe::be<uint32_t> size;
xe::be<uint32_t> property_id;
xe::be<uint32_t> flags;
xe::be<uint16_t> attribute_id;
xe::be<uint16_t> string_id;
xe::be<uint16_t> aggregation_type;
xe::be<uint8_t> ordinal;
xe::be<uint8_t> field_type;
xe::be<uint32_t> format_type;
xe::be<uint32_t> unused_1;
xe::be<uint32_t> unused_2;
};
static_assert_size(XdbfViewFieldEntry, 0x20);
struct XdbfSharedViewMetaTableEntry {
xe::be<uint16_t> column_count;
xe::be<uint16_t> row_count;
xe::be<uint32_t> unused_1;
xe::be<uint32_t> unused_2;
};
static_assert_size(XdbfSharedViewMetaTableEntry, 0xC);
#pragma pack(pop)
struct XdbfPropertyBag {
std::vector<xe::be<uint32_t>> contexts;
std::vector<xe::be<uint32_t>> properties;
};
struct XdbfSharedView {
std::vector<XdbfViewFieldEntry> column_entries;
std::vector<XdbfViewFieldEntry> 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<XdbfAchievementTableEntry> GetAchievements() const;
std::vector<XdbfPropertyTableEntry> GetProperties() const;
std::vector<XdbfContextTableEntry> 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<XdbfViewTable> 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<xe::be<uint32_t>>& contexts,
std::vector<xe::be<uint32_t>>& 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_

View File

@ -27,7 +27,7 @@ union AttributeKey {
};
enum class X_USER_DATA_TYPE : uint8_t {
CONTENT = 0,
CONTEXT = 0,
INT32 = 1,
INT64 = 2,
DOUBLE = 3,
@ -56,7 +56,7 @@ struct X_USER_DATA {
be<uint32_t> ptr;
} binary;
be<uint64_t> filetime;
};
} data;
};
static_assert_size(X_USER_DATA, 16);
@ -96,7 +96,7 @@ class Int32UserData : public UserData {
: UserData(X_USER_DATA_TYPE::INT32), value_(value) {}
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->s32 = value_;
data->data.s32 = value_;
}
private:
@ -109,7 +109,7 @@ class Uint32UserData : public UserData {
: UserData(X_USER_DATA_TYPE::INT32), value_(value) {}
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->u32 = value_;
data->data.u32 = value_;
}
private:
@ -122,7 +122,7 @@ class Int64UserData : public UserData {
: UserData(X_USER_DATA_TYPE::INT64), value_(value) {}
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->s64 = value_;
data->data.s64 = value_;
}
private:
@ -136,7 +136,7 @@ class FloatUserData : public UserData {
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->f32 = value_;
data->data.f32 = value_;
}
private:
@ -149,7 +149,7 @@ class DoubleUserData : public UserData {
: UserData(X_USER_DATA_TYPE::DOUBLE), value_(value) {}
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->f64 = value_;
data->data.f64 = value_;
}
private:
@ -164,16 +164,16 @@ class UnicodeUserData : public UserData {
UserData::Append(data, stream);
if (value_.empty()) {
data->unicode.size = 0;
data->unicode.ptr = 0;
data->data.unicode.size = 0;
data->data.unicode.ptr = 0;
return;
}
size_t count = value_.size() + 1;
size_t size = 2 * count;
assert_true(size <= std::numeric_limits<uint32_t>::max());
data->unicode.size = static_cast<uint32_t>(size);
data->unicode.ptr = stream->ptr();
data->data.unicode.size = static_cast<uint32_t>(size);
data->data.unicode.ptr = stream->ptr();
auto buffer =
reinterpret_cast<uint16_t*>(&stream->data()[stream->offset()]);
stream->Advance(size);
@ -192,15 +192,15 @@ class BinaryUserData : public UserData {
UserData::Append(data, stream);
if (value_.empty()) {
data->binary.size = 0;
data->binary.ptr = 0;
data->data.binary.size = 0;
data->data.binary.ptr = 0;
return;
}
size_t size = value_.size();
assert_true(size <= std::numeric_limits<uint32_t>::max());
data->binary.size = static_cast<uint32_t>(size);
data->binary.ptr = stream->ptr();
data->data.binary.size = static_cast<uint32_t>(size);
data->data.binary.ptr = stream->ptr();
stream->Write(value_.data(), size);
}
@ -221,7 +221,7 @@ class DateTimeUserData : public UserData {
void Append(X_USER_DATA* data, DataByteStream* stream) override {
UserData::Append(data, stream);
data->filetime = value_;
data->data.filetime = value_;
}
private:

View File

@ -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<std::u16string>(
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<uint32_t>(AchievementFlags::kAchieved) |
static_cast<uint32_t>(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<Achievement> 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<uint32_t>(AchievementFlags::kAchieved)) != 0;
}
const std::vector<AchievementGpdStructure>*
GpdAchievementBackend::GetTitleAchievements(const uint64_t xuid,
const uint32_t title_id) const {
const std::vector<Achievement> 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<const uint8_t> 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<XLanguage>(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;
}

View File

@ -15,8 +15,8 @@
#include <string>
#include <vector>
#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<Achievement> GetAchievementInfo(
const uint64_t xuid, const uint32_t title_id,
const uint32_t achievement_id) const override;
const std::vector<AchievementGpdStructure>* GetTitleAchievements(
const std::vector<Achievement> 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<const uint8_t> 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

View File

@ -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<Achievement> 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<AchievementGpdStructure>*
AchievementManager::GetTitleAchievements(const uint64_t xuid,
const uint32_t title_id) const {
const std::vector<Achievement> AchievementManager::GetTitleAchievements(
const uint64_t xuid, const uint32_t title_id) const {
return default_achievements_backend_->GetTitleAchievements(xuid, title_id);
}
const std::span<const uint8_t> 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<TitleAchievementsProfileInfo>
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<uint32_t>(achievements->size());
info.achievements_count = static_cast<uint32_t>(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<std::u16string>(
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 =

View File

@ -16,7 +16,8 @@
#include <vector>
#include "xenia/base/chrono.h"
#include "xenia/kernel/util/xdbf_utils.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
#include "xenia/kernel/xam/xdbf/spa_info.h"
#include "xenia/xbox.h"
namespace xe {
@ -96,6 +97,53 @@ struct X_ACHIEVEMENT_DETAILS {
};
static_assert_size(X_ACHIEVEMENT_DETAILS, 36);
// Host structures
struct AchievementDetails {
uint32_t id;
std::u16string label;
std::u16string description;
std::u16string unachieved;
uint32_t image_id;
uint32_t gamerscore;
X_ACHIEVEMENT_UNLOCK_TIME unlock_time;
uint32_t flags;
AchievementDetails(uint32_t id, std::u16string label,
std::u16string description, std::u16string unachieved,
uint32_t image_id, uint32_t gamerscore,
X_ACHIEVEMENT_UNLOCK_TIME unlock_time, uint32_t flags)
: id(id),
label(label),
description(description),
unachieved(unachieved),
image_id(image_id),
gamerscore(gamerscore),
unlock_time(unlock_time),
flags(flags) {};
AchievementDetails(const AchievementTableEntry* entry,
const SpaInfo* spa_data, const XLanguage language) {
id = entry->id;
image_id = entry->image_id;
gamerscore = entry->gamerscore;
flags = entry->flags;
unlock_time = {};
label =
xe::to_utf16(spa_data->GetStringTableEntry(language, entry->label_id));
description = xe::to_utf16(
spa_data->GetStringTableEntry(language, entry->description_id));
unachieved = xe::to_utf16(
spa_data->GetStringTableEntry(language, entry->unachieved_id));
}
};
struct TitleAchievementsProfileInfo {
uint32_t achievements_count;
uint32_t unlocked_achievements_count;
uint32_t gamerscore;
};
// This is structure used inside GPD file.
// GPD is writeable XDBF.
// There are two info instances
@ -103,18 +151,30 @@ static_assert_size(X_ACHIEVEMENT_DETAILS, 36);
// booted game (name, title_id, last boot time etc)
// 2. In specific Title ID directory GPD contains there structure below for
// every achievement. (unlocked or not)
struct AchievementGpdStructure {
AchievementGpdStructure(const XLanguage language,
const util::XdbfGameData xdbf,
const util::XdbfAchievementTableEntry& xdbf_entry) {
const std::string label =
xdbf.GetStringTableEntry(language, xdbf_entry.label_id);
const std::string desc =
xdbf.GetStringTableEntry(language, xdbf_entry.description_id);
const std::string locked_desc =
xdbf.GetStringTableEntry(language, xdbf_entry.unachieved_id);
struct Achievement {
Achievement() {};
Achievement(const X_XDBF_GPD_ACHIEVEMENT* xdbf_ach) {
if (!xdbf_ach) {
return;
}
achievement_id = xdbf_ach->id;
image_id = xdbf_ach->image_id;
flags = xdbf_ach->flags;
gamerscore = xdbf_ach->gamerscore;
unlock_time = static_cast<uint64_t>(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<xe::be<uint32_t>>(xdbf_entry.id);
image_id = xdbf_entry.image_id;
gamerscore = static_cast<xe::be<uint32_t>>(xdbf_entry.gamerscore);
@ -128,11 +188,10 @@ struct AchievementGpdStructure {
xe::load_and_swap<std::u16string>(xe::to_utf16(locked_desc).c_str());
}
xe::be<uint32_t> struct_size;
xe::be<uint32_t> achievement_id;
xe::be<uint32_t> image_id;
xe::be<uint32_t> gamerscore;
xe::be<uint32_t> flags;
uint32_t achievement_id;
uint32_t image_id;
uint32_t gamerscore;
uint32_t flags;
X_ACHIEVEMENT_UNLOCK_TIME unlock_time;
std::u16string achievement_name;
std::u16string unlocked_description;
@ -144,12 +203,6 @@ struct AchievementGpdStructure {
}
};
struct TitleAchievementsProfileInfo {
uint32_t achievements_count;
uint32_t unlocked_achievements_count;
uint32_t gamerscore;
};
class AchievementBackendInterface {
public:
virtual ~AchievementBackendInterface() {};
@ -161,44 +214,47 @@ class AchievementBackendInterface {
const uint32_t title_id,
const uint32_t achievement_id) const = 0;
virtual const AchievementGpdStructure* GetAchievementInfo(
virtual const std::optional<Achievement> GetAchievementInfo(
const uint64_t xuid, const uint32_t title_id,
const uint32_t achievement_id) const = 0;
virtual const std::vector<AchievementGpdStructure>* GetTitleAchievements(
virtual const std::vector<Achievement> 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<const uint8_t> 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<Achievement> GetAchievementInfo(
const uint64_t xuid, const uint32_t title_id,
const uint32_t achievement_id) const;
const std::vector<AchievementGpdStructure>* GetTitleAchievements(
const std::vector<Achievement> GetTitleAchievements(
const uint64_t xuid, const uint32_t title_id) const;
const std::optional<TitleAchievementsProfileInfo> GetTitleAchievementsInfo(
const uint64_t xuid, const uint32_t title_id) const;
const std::span<const uint8_t> 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<std::unique_ptr<AchievementBackendInterface>>

View File

@ -43,22 +43,11 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
uint32_t context_value = xe::load_and_swap<uint32_t>(buffer + 20);
XELOGD("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index,
context_id, context_value);
const util::XdbfGameData title_xdbf = kernel_state_->title_xdbf();
if (title_xdbf.is_valid()) {
const auto context = title_xdbf.GetContext(context_id);
const XLanguage title_language = title_xdbf.GetExistingLanguage(
static_cast<XLanguage>(XLanguage::kEnglish));
const std::string desc =
title_xdbf.GetStringTableEntry(title_language, context.string_id);
XELOGD("XGIUserSetContextEx: {} - Set to value: {}", desc,
context_value);
UserProfile* user_profile =
kernel_state_->xam_state()->GetUserProfile(user_index);
if (user_profile) {
user_profile->contexts_[context_id] = context_value;
}
UserProfile* user_profile =
kernel_state_->xam_state()->GetUserProfile(user_index);
if (user_profile) {
kernel_state_->xam_state()->user_tracker()->UpdateContext(
user_profile->xuid(), context_id, context_value);
}
return X_E_SUCCESS;
}
@ -70,25 +59,14 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", user_index,
property_id, value_size, value_ptr);
const util::XdbfGameData title_xdbf = kernel_state_->title_xdbf();
if (title_xdbf.is_valid()) {
const auto property_xdbf = title_xdbf.GetProperty(property_id);
const XLanguage title_language = title_xdbf.GetExistingLanguage(
static_cast<XLanguage>(XLanguage::kEnglish));
const std::string desc = title_xdbf.GetStringTableEntry(
title_language, property_xdbf.string_id);
Property property(property_id, value_size,
memory_->TranslateVirtual<uint8_t*>(value_ptr));
Property property =
Property(property_id, value_size,
memory_->TranslateVirtual<uint8_t*>(value_ptr));
auto user = kernel_state_->xam_state()->GetUserProfile(user_index);
if (user) {
user->AddProperty(&property);
}
XELOGD("XGIUserSetPropertyEx: Setting property: {}", desc);
auto user = kernel_state_->xam_state()->GetUserProfile(user_index);
if (user) {
kernel_state_->xam_state()->user_tracker()->AddProperty(user->xuid(),
&property);
}
return X_E_SUCCESS;
}
case 0x000B0008: {
@ -202,9 +180,12 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
UserProfile* user_profile =
kernel_state_->xam_state()->GetUserProfile(user_index);
if (user_profile) {
if (user_profile->contexts_.find(context_id) !=
user_profile->contexts_.cend()) {
value = user_profile->contexts_[context_id];
auto result =
kernel_state_->xam_state()->user_tracker()->GetUserContext(
user_profile->xuid(), context_id);
if (result) {
value = result.value();
}
}
xe::store_and_swap<uint32_t>(context + 4, value);

View File

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

View File

@ -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<UserProfile>(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) {

View File

@ -26,6 +26,14 @@ class KernelState;
} // namespace kernel
} // namespace xe
namespace xe {
namespace kernel {
namespace xam {
class UserTracker;
} // namespace xam
} // namespace kernel
} // namespace xe
namespace xe {
namespace kernel {
namespace xam {
@ -75,7 +83,7 @@ class ProfileManager {
// Loading Profile means load everything
// Loading Account means load basic data
ProfileManager(KernelState* kernel_state);
ProfileManager(KernelState* kernel_state, UserTracker* user_tracker);
~ProfileManager();
@ -142,6 +150,7 @@ class ProfileManager {
std::map<uint8_t, std::unique_ptr<UserProfile>> logged_profiles_;
KernelState* kernel_state_;
UserTracker* user_tracker_;
};
} // namespace xam

View File

@ -9,11 +9,13 @@
#include "xenia/kernel/xam/user_profile.h"
#include <ranges>
#include <sstream>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
namespace xe {
namespace kernel {
@ -24,240 +26,60 @@ UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info)
// 58410A1F checks the user XUID against a mask of 0x00C0000000000000 (3<<54),
// if non-zero, it prevents the user from playing the game.
// "You do not have permissions to perform this operation."
// https://cs.rin.ru/forum/viewtopic.php?f=38&t=60668&hilit=gfwl+live&start=195
// https://github.com/arkem/py360/blob/master/py360/constants.py
// XPROFILE_GAMER_YAXIS_INVERSION
AddSetting(std::make_unique<UserSetting>(0x10040002, 0));
// XPROFILE_OPTION_CONTROLLER_VIBRATION
AddSetting(std::make_unique<UserSetting>(0x10040003, 3));
// XPROFILE_GAMERCARD_ZONE
AddSetting(std::make_unique<UserSetting>(0x10040004, 0));
// XPROFILE_GAMERCARD_REGION
AddSetting(std::make_unique<UserSetting>(0x10040005, 0));
// XPROFILE_GAMERCARD_CRED
AddSetting(std::make_unique<UserSetting>(0x10040006, 0xFA));
// XPROFILE_OPTION_VOICE_MUTED
AddSetting(std::make_unique<UserSetting>(0x1004000C, 3));
// XPROFILE_OPTION_VOICE_THRU_SPEAKERS
AddSetting(std::make_unique<UserSetting>(0x1004000D, 3));
// XPROFILE_OPTION_VOICE_VOLUME
AddSetting(std::make_unique<UserSetting>(0x1004000E, 0x64));
// XPROFILE_GAMERCARD_TITLES_PLAYED
AddSetting(std::make_unique<UserSetting>(0x10040012, 1));
// XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED
AddSetting(std::make_unique<UserSetting>(0x10040013, 0));
// XPROFILE_GAMER_DIFFICULTY
AddSetting(std::make_unique<UserSetting>(0x10040015, 0));
// XPROFILE_GAMER_CONTROL_SENSITIVITY
AddSetting(std::make_unique<UserSetting>(0x10040018, 0));
// Preferred color 1
AddSetting(std::make_unique<UserSetting>(0x1004001D, PREFERRED_COLOR_NONE));
// Preferred color 2
AddSetting(std::make_unique<UserSetting>(0x1004001E, PREFERRED_COLOR_NONE));
// XPROFILE_GAMER_ACTION_AUTO_AIM
AddSetting(std::make_unique<UserSetting>(0x10040022, 1));
// XPROFILE_GAMER_ACTION_AUTO_CENTER
AddSetting(std::make_unique<UserSetting>(0x10040023, 0));
// XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL
AddSetting(std::make_unique<UserSetting>(0x10040024, 0));
// XPROFILE_GAMER_RACE_TRANSMISSION
AddSetting(std::make_unique<UserSetting>(0x10040026, 0));
// XPROFILE_GAMER_RACE_CAMERA_LOCATION
AddSetting(std::make_unique<UserSetting>(0x10040027, 0));
// XPROFILE_GAMER_RACE_BRAKE_CONTROL
AddSetting(std::make_unique<UserSetting>(0x10040028, 0));
// XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL
AddSetting(std::make_unique<UserSetting>(0x10040029, 0));
// XPROFILE_GAMERCARD_TITLE_CRED_EARNED
AddSetting(std::make_unique<UserSetting>(0x10040038, 0));
// XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED
AddSetting(std::make_unique<UserSetting>(0x10040039, 0));
// XPROFILE_GAMERCARD_MOTTO
AddSetting(std::make_unique<UserSetting>(0x402C0011, u""));
// XPROFILE_GAMERCARD_PICTURE_KEY
AddSetting(
std::make_unique<UserSetting>(0x4064000F, u"gamercard_picture_key"));
// XPROFILE_GAMERCARD_REP
AddSetting(std::make_unique<UserSetting>(0x5004000B, 0.0f));
// XPROFILE_TITLE_SPECIFIC1
AddSetting(std::make_unique<UserSetting>(0x63E83FFF, std::vector<uint8_t>()));
// XPROFILE_TITLE_SPECIFIC2
AddSetting(std::make_unique<UserSetting>(0x63E83FFE, std::vector<uint8_t>()));
// XPROFILE_TITLE_SPECIFIC3
AddSetting(std::make_unique<UserSetting>(0x63E83FFD, std::vector<uint8_t>()));
LoadProfileGpds();
}
void UserProfile::AddSetting(std::unique_ptr<UserSetting> setting) {
UserSetting* previous_setting = setting.get();
std::swap(settings_[setting->GetSettingId()], previous_setting);
if (setting->is_title_specific()) {
SaveSetting(setting.get());
void UserProfile::LoadProfileGpds() {
// First load dashboard GPD because it stores all opened games
dashboard_gpd_ = LoadGpd(kDashboardID);
if (!dashboard_gpd_.IsValid()) {
dashboard_gpd_ = GpdInfoProfile();
}
if (previous_setting) {
// replace: swap out the old setting from the owning list
for (auto vec_it = setting_list_.begin(); vec_it != setting_list_.end();
++vec_it) {
if (vec_it->get() == previous_setting) {
vec_it->swap(setting);
break;
}
}
} else {
// new setting: add to the owning list
setting_list_.push_back(std::move(setting));
}
}
const auto gpds_to_load = dashboard_gpd_.GetTitlesInfo();
UserSetting* UserProfile::GetSetting(uint32_t setting_id) {
const auto& it = settings_.find(setting_id);
if (it == settings_.end()) {
return nullptr;
}
UserSetting* setting = it->second;
if (setting->is_title_specific()) {
// If what we have loaded in memory isn't for the title that is running
// right now, then load it from disk.
LoadSetting(setting);
}
return setting;
}
void UserProfile::LoadSetting(UserSetting* setting) {
if (setting->is_title_specific()) {
const std::filesystem::path content_dir =
kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_);
const std::string setting_id_str =
fmt::format("{:08X}", setting->GetSettingId());
const std::filesystem::path file_path = content_dir / setting_id_str;
FILE* file = xe::filesystem::OpenFile(file_path, "rb");
if (!file) {
return;
}
const uint32_t input_file_size =
static_cast<uint32_t>(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<uint8_t> 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<uint8_t> serialized_setting =
setting->GetSettingData()->Serialize();
const uint32_t serialized_setting_length = std::min(
kMaxSettingSize, static_cast<uint32_t>(serialized_setting.size()));
fwrite(setting->GetSettingHeader(), sizeof(X_USER_PROFILE_SETTING_HEADER),
1, file);
// Writing data
fwrite(serialized_setting.data(), 1, serialized_setting_length, file);
fclose(file);
} else {
// Unsupported for now. Other settings aren't per-game and need to be
// stored some other way.
XELOGW("Attempting to save unsupported profile setting 0x{:08X} from disk",
setting->GetSettingId());
}
}
bool UserProfile::AddProperty(const Property* property) {
// Find if property already exits
Property* entry = GetProperty(property->GetPropertyId());
if (entry) {
*entry = *property;
return true;
}
properties_.push_back(*property);
return true;
}
Property* UserProfile::GetProperty(const AttributeKey id) {
for (auto& entry : properties_) {
if (entry.GetPropertyId().value != id.value) {
for (const auto gpd : gpds_to_load) {
const auto gpd_data = LoadGpd(gpd->title_id);
if (gpd_data.empty()) {
continue;
}
return &entry;
games_gpd_.emplace(gpd->title_id, GpdInfoTitle(gpd->title_id, gpd_data));
}
return nullptr;
}
AchievementGpdStructure* UserProfile::GetAchievement(const uint32_t title_id,
const uint32_t id) {
auto title_achievements = achievements_.find(title_id);
if (title_achievements == achievements_.end()) {
return nullptr;
std::vector<uint8_t> UserProfile::LoadGpd(const uint32_t title_id) {
auto entry = kernel_state()->file_system()->ResolvePath(
fmt::format("{:016X}:\\{:08X}.gpd", xuid_, title_id));
if (!entry) {
XELOGW("User {} (XUID: {:016X}) doesn't have profile GPD!", name(), xuid());
return {};
}
for (auto& entry : title_achievements->second) {
if (entry.achievement_id == id) {
return &entry;
}
vfs::File* file;
auto result = entry->Open(vfs::FileAccess::kFileReadData, &file);
if (result != X_STATUS_SUCCESS) {
XELOGW("User {} (XUID: {:016X}) cannot open profile GPD!", name(), xuid());
return {};
}
return nullptr;
std::vector<uint8_t> data(entry->size());
size_t read_size = 0;
result = file->ReadSync(data.data(), entry->size(), 0, &read_size);
if (result != X_STATUS_SUCCESS || read_size != entry->size()) {
XELOGW(
"User {} (XUID: {:016X}) cannot read profile GPD! Status: {:08X} read: "
"{}/{} bytes",
name(), xuid(), result, read_size, entry->size());
return {};
}
return data;
}
std::vector<AchievementGpdStructure>* UserProfile::GetTitleAchievements(
const uint32_t title_id) {
auto title_achievements = achievements_.find(title_id);
if (title_achievements == achievements_.end()) {
return nullptr;
}
return &title_achievements->second;
}
bool UserProfile::WriteGpd(const uint32_t title_id) { return false; }
} // namespace xam
} // namespace kernel

View File

@ -16,63 +16,25 @@
#include <unordered_map>
#include <vector>
#include "xenia/base/byte_stream.h"
#include "xenia/kernel/util/property.h"
#include "xenia/kernel/util/xuserdata.h"
#include "xenia/kernel/xam/achievement_manager.h"
#include "xenia/kernel/xam/xdbf/gpd_info_profile.h"
#include "xenia/kernel/xam/xdbf/gpd_info_title.h"
#include "xenia/xbox.h"
namespace xe {
namespace kernel {
namespace xam {
constexpr uint32_t kMaxSettingSize = 0x03E8;
enum class X_USER_PROFILE_SETTING_SOURCE : uint32_t {
NOT_SET = 0,
DEFAULT = 1,
TITLE = 2,
DEFAULT = 1, // Default value taken from default OS values.
TITLE = 2, // Value written by title or OS.
UNKNOWN = 3,
};
enum PREFERRED_COLOR_OPTIONS : uint32_t {
PREFERRED_COLOR_NONE,
PREFERRED_COLOR_BLACK,
PREFERRED_COLOR_WHITE,
PREFERRED_COLOR_YELLOW,
PREFERRED_COLOR_ORANGE,
PREFERRED_COLOR_PINK,
PREFERRED_COLOR_RED,
PREFERRED_COLOR_PURPLE,
PREFERRED_COLOR_BLUE,
PREFERRED_COLOR_GREEN,
PREFERRED_COLOR_BROWN,
PREFERRED_COLOR_SILVER
};
// Each setting contains 0x18 bytes long header
struct X_USER_PROFILE_SETTING_HEADER {
xe::be<uint32_t> setting_id;
xe::be<uint32_t> unknown_1;
xe::be<uint8_t> setting_type;
char unknown_2[3];
xe::be<uint32_t> unknown_3;
union {
// Size is used only for types: CONTENT, WSTRING, BINARY
be<uint32_t> size;
// Raw values that can be written. They do not need to be serialized.
be<int32_t> s32;
be<int64_t> s64;
be<uint32_t> u32;
be<double> f64;
be<float> f32;
};
};
static_assert_size(X_USER_PROFILE_SETTING_HEADER, 0x18);
struct X_USER_PROFILE_SETTING {
xe::be<uint32_t> from;
xe::be<X_USER_PROFILE_SETTING_SOURCE> source;
union {
xe::be<uint32_t> user_index;
xe::be<uint64_t> xuid;
@ -85,88 +47,6 @@ struct X_USER_PROFILE_SETTING {
};
static_assert_size(X_USER_PROFILE_SETTING, 40);
class UserSetting {
public:
template <typename T>
UserSetting(uint32_t setting_id, T data) {
header_.setting_id = setting_id;
setting_id_.value = setting_id;
CreateUserData(setting_id, data);
}
static bool is_title_specific(uint32_t setting_id) {
return (setting_id & 0x3F00) == 0x3F00;
}
bool is_title_specific() const {
return is_title_specific(setting_id_.value);
}
const uint32_t GetSettingId() const { return setting_id_.value; }
const X_USER_PROFILE_SETTING_SOURCE GetSettingSource() const {
return created_by_;
}
const X_USER_PROFILE_SETTING_HEADER* GetSettingHeader() const {
return &header_;
}
UserData* GetSettingData() { return user_data_.get(); }
void SetNewSettingSource(X_USER_PROFILE_SETTING_SOURCE new_source) {
created_by_ = new_source;
}
void SetNewSettingHeader(X_USER_PROFILE_SETTING_HEADER* header) {
header_ = *header;
}
private:
void CreateUserData(uint32_t setting_id, uint32_t data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::INT32);
header_.u32 = data;
user_data_ = std::make_unique<Uint32UserData>(data);
}
void CreateUserData(uint32_t setting_id, int32_t data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::INT32);
header_.s32 = data;
user_data_ = std::make_unique<Int32UserData>(data);
}
void CreateUserData(uint32_t setting_id, float data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::FLOAT);
header_.f32 = data;
user_data_ = std::make_unique<FloatUserData>(data);
}
void CreateUserData(uint32_t setting_id, double data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::DOUBLE);
header_.f64 = data;
user_data_ = std::make_unique<DoubleUserData>(data);
}
void CreateUserData(uint32_t setting_id, int64_t data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::INT64);
header_.s64 = data;
user_data_ = std::make_unique<Int64UserData>(data);
}
void CreateUserData(uint32_t setting_id, const std::u16string& data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::WSTRING);
header_.size =
std::min(kMaxSettingSize, static_cast<uint32_t>((data.size() + 1) * 2));
user_data_ = std::make_unique<UnicodeUserData>(data);
}
void CreateUserData(uint32_t setting_id, const std::vector<uint8_t>& data) {
header_.setting_type = static_cast<uint8_t>(X_USER_DATA_TYPE::BINARY);
header_.size =
std::min(kMaxSettingSize, static_cast<uint32_t>(data.size()));
user_data_ = std::make_unique<BinaryUserData>(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<UserData> user_data_ = nullptr;
};
class UserProfile {
public:
UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info);
@ -185,34 +65,22 @@ class UserProfile {
sizeof(account_info_.passcode));
};
void AddSetting(std::unique_ptr<UserSetting> setting);
UserSetting* GetSetting(uint32_t setting_id);
bool AddProperty(const Property* property);
Property* GetProperty(const AttributeKey id);
std::map<uint32_t, uint32_t> contexts_;
friend class UserTracker;
friend class GpdAchievementBackend;
protected:
AchievementGpdStructure* GetAchievement(const uint32_t title_id,
const uint32_t id);
std::vector<AchievementGpdStructure>* GetTitleAchievements(
const uint32_t title_id);
private:
uint64_t xuid_;
X_XAMACCOUNTINFO account_info_;
std::vector<std::unique_ptr<UserSetting>> setting_list_;
std::unordered_map<uint32_t, UserSetting*> settings_;
std::map<uint32_t, std::vector<AchievementGpdStructure>> achievements_;
GpdInfoProfile dashboard_gpd_;
std::map<uint32_t, GpdInfoTitle> games_gpd_;
std::map<uint32_t, uint32_t> contexts_;
std::vector<Property> properties_;
void LoadSetting(UserSetting*);
void SaveSetting(UserSetting*);
void LoadProfileGpds();
std::vector<uint8_t> LoadGpd(const uint32_t title_id);
bool WriteGpd(const uint32_t title_id);
};
} // namespace xam

View File

@ -0,0 +1,190 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <ranges>
#include <sstream>
#include "xenia/kernel/xam/user_settings.h"
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
namespace xe {
namespace kernel {
namespace xam {
UserSetting::UserSetting(UserSettingId setting_id, SettingTypes setting_data)
: setting_id_(setting_id),
setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {
setting_type_ = get_setting_type(static_cast<uint32_t>(setting_id));
max_size_ = get_setting_max_size(static_cast<uint32_t>(setting_id));
user_data_ = {};
user_data_.type = setting_type_;
switch (setting_type_) {
case X_USER_DATA_TYPE::BINARY:
extended_data_ = std::get<std::vector<uint8_t>>(setting_data);
size_ = static_cast<uint16_t>(extended_data_.size());
user_data_.data.binary.size = size_;
break;
case X_USER_DATA_TYPE::WSTRING: {
std::u16string str = std::get<std::u16string>(setting_data);
size_ = static_cast<uint16_t>(string_util::size_in_bytes(str));
user_data_.data.unicode.size = size_;
extended_data_.resize(size_);
memcpy(extended_data_.data(), reinterpret_cast<uint8_t*>(str.data()),
size_);
break;
}
case X_USER_DATA_TYPE::INT32:
user_data_.data.s32 = std::get<int32_t>(setting_data);
size_ = sizeof(int32_t);
break;
case X_USER_DATA_TYPE::FLOAT:
user_data_.data.f32 = std::get<float>(setting_data);
size_ = sizeof(int32_t);
break;
case X_USER_DATA_TYPE::CONTEXT:
user_data_.data.s32 = std::get<int32_t>(setting_data);
size_ = sizeof(int32_t);
break;
case X_USER_DATA_TYPE::DOUBLE:
user_data_.data.f64 = std::get<double>(setting_data);
size_ = sizeof(int64_t);
break;
case X_USER_DATA_TYPE::DATETIME:
case X_USER_DATA_TYPE::INT64:
user_data_.data.s64 = std::get<int64_t>(setting_data);
size_ = sizeof(int64_t);
break;
default:
assert_always();
}
}
UserSetting::UserSetting(const X_USER_PROFILE_SETTING* profile_setting)
: setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {
setting_id_ = static_cast<UserSettingId>(profile_setting->setting_id.get());
setting_type_ = get_setting_type(static_cast<uint32_t>(setting_id_));
max_size_ = get_setting_max_size(static_cast<uint32_t>(setting_id_));
size_ = static_cast<uint16_t>(get_setting_data_size(profile_setting));
// Set that union to zero
user_data_.data.s64 = 0;
switch (setting_type_) {
case X_USER_DATA_TYPE::WSTRING:
case X_USER_DATA_TYPE::BINARY: {
user_data_.data.binary.size = profile_setting->data.data.binary.size;
extended_data_.resize(profile_setting->data.data.binary.size);
memcpy(extended_data_.data(),
kernel_memory()->TranslateVirtual<uint8_t*>(
profile_setting->data.data.binary.ptr),
size_);
break;
}
case X_USER_DATA_TYPE::INT32:
user_data_.data.s32 = profile_setting->data.data.s32;
break;
case X_USER_DATA_TYPE::FLOAT:
user_data_.data.f32 = profile_setting->data.data.f32;
break;
case X_USER_DATA_TYPE::CONTEXT:
user_data_.data.u32 = profile_setting->data.data.u32;
break;
case X_USER_DATA_TYPE::DATETIME:
user_data_.data.filetime = profile_setting->data.data.filetime;
break;
case X_USER_DATA_TYPE::DOUBLE:
user_data_.data.f64 = profile_setting->data.data.f64;
break;
case X_USER_DATA_TYPE::INT64:
user_data_.data.s64 = profile_setting->data.data.s64;
break;
default:
assert_always();
}
}
UserSetting::UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting,
std::span<const uint8_t> extended_data)
: setting_source_(X_USER_PROFILE_SETTING_SOURCE::TITLE) {
setting_id_ = static_cast<UserSettingId>(profile_setting->setting_id.get());
setting_type_ = get_setting_type(static_cast<uint32_t>(setting_id_));
max_size_ = get_setting_max_size(static_cast<uint32_t>(setting_id_));
size_ = 0;
user_data_ = {};
user_data_.type = setting_type_;
switch (setting_type_) {
case X_USER_DATA_TYPE::WSTRING:
case X_USER_DATA_TYPE::BINARY: {
user_data_.data.binary.ptr = 0;
size_ = profile_setting->base_data.size;
user_data_.data.binary.size = size_;
extended_data_.resize(size_);
const uint8_t* ptr = reinterpret_cast<const uint8_t*>(profile_setting) +
sizeof(X_XDBF_GPD_SETTING_HEADER);
memcpy(extended_data_.data(), ptr, size_);
break;
}
case X_USER_DATA_TYPE::INT32:
user_data_.data.s32 = profile_setting->base_data.s32;
break;
case X_USER_DATA_TYPE::FLOAT:
user_data_.data.f32 = profile_setting->base_data.f32;
break;
case X_USER_DATA_TYPE::CONTEXT:
user_data_.data.u32 = profile_setting->base_data.u32;
break;
case X_USER_DATA_TYPE::INT64:
case X_USER_DATA_TYPE::DATETIME:
user_data_.data.s64 = profile_setting->base_data.s64;
break;
case X_USER_DATA_TYPE::DOUBLE:
user_data_.data.f64 = profile_setting->base_data.f64;
break;
default:
assert_always();
}
}
std::vector<uint8_t> UserSetting::serialize_to_gpd() const {
std::vector<uint8_t> data(sizeof(X_XDBF_GPD_SETTING_HEADER) +
extended_data_.size());
X_XDBF_GPD_SETTING_HEADER header = {};
header.setting_id = static_cast<uint32_t>(setting_id_);
header.setting_type = setting_type_;
memcpy(&header.base_data, &user_data_.data, 8);
// Copy header to vector
memcpy(data.data(), &header, sizeof(X_XDBF_GPD_SETTING_HEADER));
memcpy(data.data() + sizeof(X_XDBF_GPD_SETTING_HEADER), extended_data_.data(),
extended_data_.size());
return data;
}
const X_USER_DATA* UserSetting::get_base_data() { return &user_data_; }
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -0,0 +1,502 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XAM_USER_SETTINGS_H_
#define XENIA_KERNEL_XAM_USER_SETTINGS_H_
#include <array>
#include <set>
#include <variant>
#include <vector>
#include "xenia/kernel/util/xuserdata.h"
#include "xenia/kernel/xam/profile_manager.h"
#include "xenia/kernel/xam/user_profile.h"
#include "xenia/xbox.h"
namespace xe {
namespace kernel {
namespace xam {
constexpr uint32_t kMaxSettingSize = 0x03E8;
constexpr uint32_t SettingKey(X_USER_DATA_TYPE type, uint16_t size,
uint16_t id) {
return static_cast<uint32_t>(type) << 28 | size << 16 | id;
}
enum class UserSettingId : uint32_t {
XPROFILE_PERMISSIONS =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0),
XPROFILE_GAMER_TYPE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
1), // 0x10040001,
XPROFILE_GAMER_YAXIS_INVERSION =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 2), // 0x10040002,
XPROFILE_OPTION_CONTROLLER_VIBRATION =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 3), // 0x10040003,
XPROFILE_GAMERCARD_ZONE =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 4), // 0x10040004,
XPROFILE_GAMERCARD_REGION =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 5), // 0x10040005,
XPROFILE_GAMERCARD_CRED =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 6), // 0x10040006,
XPROFILE_GAMER_PRESENCE_USER_STATE =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 7), // 0x10040007,
XPROFILE_GAMERCARD_HAS_VISION =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 8), // 0x10040008,
XPROFILE_OPTION_VOICE_MUTED = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xC), // 0x1004000C,
XPROFILE_OPTION_VOICE_THRU_SPEAKERS = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xD), // 0x1004000D,
XPROFILE_OPTION_VOICE_VOLUME = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0xE), // 0x1004000E,
XPROFILE_GAMERCARD_TITLES_PLAYED = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x12), // 0x10040012,
XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x13), // 0x10040013,
XPROFILE_GAMER_DIFFICULTY = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x15), // 0x10040015,
XPROFILE_GAMER_CONTROL_SENSITIVITY = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x18), // 0x10040018,
XPROFILE_GAMER_PREFERRED_COLOR_FIRST = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x1D), // 0x1004001D,
XPROFILE_GAMER_PREFERRED_COLOR_SECOND = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x1E), // 0x1004001E,
XPROFILE_GAMER_ACTION_AUTO_AIM = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x22), // 0x10040022,
XPROFILE_GAMER_ACTION_AUTO_CENTER = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x23), // 0x10040023,
XPROFILE_GAMER_ACTION_MOVEMENT_CONTROL = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x24), // 0x10040024,
XPROFILE_GAMER_RACE_TRANSMISSION = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x26), // 0x10040026,
XPROFILE_GAMER_RACE_CAMERA_LOCATION = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x27), // 0x10040027,
XPROFILE_GAMER_RACE_BRAKE_CONTROL = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x28), // 0x10040028,
XPROFILE_GAMER_RACE_ACCELERATOR_CONTROL = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x29), // 0x10040029,
XPROFILE_GAMERCARD_TITLE_CRED_EARNED = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x38), // 0x10040038,
XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x39), // 0x10040039,
XPROFILE_GAMER_TIER = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x3A), // 0x1004003A,
XPROFILE_MESSENGER_SIGNUP_STATE = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3B), // 0x1004003B,
XPROFILE_MESSENGER_AUTO_SIGNIN = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3C), // 0x1004003C,
XPROFILE_SAVE_WINDOWS_LIVE_PASSWORD = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3D), // 0x1004003D,
XPROFILE_FRIENDSAPP_SHOW_BUDDIES = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3E), // 0x1004003E,
XPROFILE_GAMERCARD_SERVICE_TYPE_FLAGS = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F), // 0x1004003F,
XPROFILE_TENURE_LEVEL = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x47), // 0x10040047,
XPROFILE_TENURE_MILESTONE = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x48), // 0x10040048,
XPROFILE_SUBSCRIPTION_TYPE_LENGTH_IN_MONTHS = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4B), // 0x1004004B,
XPROFILE_SUBSCRIPTION_PAYMENT_TYPE = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4C), // 0x1004004C,
XPROFILE_PEC_INFO = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x4D), // 0x1004004D,
XPROFILE_NUI_BIOMETRIC_SIGNIN =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x4E), // 0x1004004E, set by XamUserNuiEnableBiometric
XPROFILE_GFWL_VADNORMAL = SettingKey(X_USER_DATA_TYPE::INT32,
sizeof(uint32_t), 0x4F), // 0x1004004F,
XPROFILE_BEACONS_SOCIAL_NETWORK_SHARING = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x52), // 0x10040052,
XPROFILE_USER_PREFERENCES = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x53), // 0x10040053,
XPROFILE_XBOXONE_GAMERSCORE =
SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x57), // 0x10040057, "XboxOneGamerscore" inside dash.xex
WEB_EMAIL_FORMAT = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2000), // 0x10042000,
WEB_FLAGS = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2001), // 0x10042001,
WEB_SPAM = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2002), // 0x10042002,
WEB_FAVORITE_GENRE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2003), // 0x10042003,
WEB_FAVORITE_GAME = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2004), // 0x10042004,
WEB_FAVORITE_GAME1 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2005), // 0x10042005,
WEB_FAVORITE_GAME2 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2006), // 0x10042006,
WEB_FAVORITE_GAME3 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2007), // 0x10042007,
WEB_FAVORITE_GAME4 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2008), // 0x10042008,
WEB_FAVORITE_GAME5 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x2009), // 0x10042009,
WEB_PLATFORMS_OWNED = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x200A), // 0x1004200A,
WEB_CONNECTION_SPEED = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x200B), // 0x1004200B,
WEB_FLASH = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x200C), // 0x1004200C,
WEB_VIDEO_PREFERENCE = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t),
0x200D), // 0x1004200D,
XPROFILE_CRUX_MEDIA_STYLE1 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EA), // 0x100403EA,
XPROFILE_CRUX_MEDIA_STYLE2 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EB), // 0x100403EB,
XPROFILE_CRUX_MEDIA_STYLE3 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EC), // 0x100403EC,
XPROFILE_CRUX_TOP_ALBUM1 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3ED), // 0x100403ED,
XPROFILE_CRUX_TOP_ALBUM2 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EE), // 0x100403EE,
XPROFILE_CRUX_TOP_ALBUM3 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3EF), // 0x100403EF,
XPROFILE_CRUX_TOP_ALBUM4 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F0), // 0x100403F0,
XPROFILE_CRUX_TOP_ALBUM5 = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F1), // 0x100403F1,
XPROFILE_CRUX_BKGD_IMAGE = SettingKey(
X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F3), // 0x100403F3,
XPROFILE_GAMERCARD_USER_LOCATION =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x52, 0x41), // 0x40520041,
XPROFILE_GAMERCARD_USER_NAME =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x104, 0x40), // 0x41040040,
XPROFILE_GAMERCARD_USER_URL =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x190, 0x42), // 0x41900042,
XPROFILE_GAMERCARD_USER_BIO = SettingKey(
X_USER_DATA_TYPE::WSTRING, kMaxSettingSize, 0x43), // 0x43E80043,
XPROFILE_CRUX_BIO = SettingKey(X_USER_DATA_TYPE::WSTRING, kMaxSettingSize,
0x3FA), // 0x43E803FA,
XPROFILE_CRUX_BG_SMALL_PRIVATE =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FB), // 0x406403FB,
XPROFILE_CRUX_BG_LARGE_PRIVATE =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FC), // 0x406403FC,
XPROFILE_CRUX_BG_SMALL_PUBLIC =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FD), // 0x406403FD,
XPROFILE_CRUX_BG_LARGE_PUBLIC =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3FE), // 0x406403FE
XPROFILE_GAMERCARD_PICTURE_KEY =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0xF), // 0x4064000F,
XPROFILE_GAMERCARD_PERSONAL_PICTURE =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x10), // 0x40640010,
XPROFILE_GAMERCARD_MOTTO =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x2C, 0x11), // 0x402C0011,
XPROFILE_GFWL_RECDEVICEDESC =
SettingKey(X_USER_DATA_TYPE::WSTRING, 200, 0x49), // 0x40C80049,
XPROFILE_GFWL_PLAYDEVICEDESC =
SettingKey(X_USER_DATA_TYPE::WSTRING, 200, 0x4B), // 0x40C8004B,
XPROFILE_CRUX_MEDIA_PICTURE =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x64, 0x3E8), // 0x406403E8,
XPROFILE_CRUX_MEDIA_MOTTO =
SettingKey(X_USER_DATA_TYPE::WSTRING, 0x100, 0x3F6), // 0x410003F6,
XPROFILE_GAMERCARD_REP =
SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float), 0xB), // 0x5004000B,
XPROFILE_GFWL_VOLUMELEVEL =
SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float), 0x4C), // 0x5004004C,
XPROFILE_GFWL_RECLEVEL = SettingKey(X_USER_DATA_TYPE::FLOAT, sizeof(float),
0x4D), // 0x5004004D,
XPROFILE_GFWL_PLAYDEVICE =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x4A), // 0x6010004A,
XPROFILE_VIDEO_METADATA =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x20, 0x4A), // 0x6020004A,
XPROFILE_CRUX_OFFLINE_ID =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x34, 0x3F2), // 0x603403F2,
XPROFILE_UNK_61180050 =
SettingKey(X_USER_DATA_TYPE::BINARY, 280, 0x50), // 0x61180050,
XPROFILE_JUMP_IN_LIST = SettingKey(X_USER_DATA_TYPE::BINARY, kMaxSettingSize,
0x51), // 0x63E80051,
XPROFILE_GAMERCARD_PARTY_ADDR =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x62, 0x54), // 0x60620054,
XPROFILE_CRUX_TOP_MUSIC =
SettingKey(X_USER_DATA_TYPE::BINARY, 0xA8, 0x3F5), // 0x60A803F5,
XPROFILE_CRUX_TOP_MEDIAID1 =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F7), // 0x601003F7,
XPROFILE_CRUX_TOP_MEDIAID2 =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F8), // 0x601003F8,
XPROFILE_CRUX_TOP_MEDIAID3 =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x10, 0x3F9), // 0x601003F9,
XPROFILE_GAMERCARD_AVATAR_INFO_1 = SettingKey(
X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x44), // 0x63E80044,
XPROFILE_GAMERCARD_AVATAR_INFO_2 = SettingKey(
X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x45), // 0x63E80045,
XPROFILE_GAMERCARD_PARTY_INFO =
SettingKey(X_USER_DATA_TYPE::BINARY, 0x100, 0x46), // 0x61000046,
XPROFILE_TITLE_SPECIFIC1 = SettingKey(
X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFF), // 0x63E83FFF,
XPROFILE_TITLE_SPECIFIC2 = SettingKey(
X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFE), // 0x63E83FFE,
XPROFILE_TITLE_SPECIFIC3 = SettingKey(
X_USER_DATA_TYPE::BINARY, kMaxSettingSize, 0x3FFD), // 0x63E83FFD,
XPROFILE_CRUX_LAST_CHANGE_TIME = SettingKey(
X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t), 0x3F4), // 0x700803F4,
XPROFILE_TENURE_NEXT_MILESTONE_DATE =
SettingKey(X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t),
0x49), // 0x70080049, aka ProfileDateTimeCreated?
XPROFILE_LAST_LIVE_SIGNIN =
SettingKey(X_USER_DATA_TYPE::DATETIME, sizeof(uint64_t),
0x4F), // 0x7008004F, named "LastOnLIVE" in Velocity
};
constexpr static std::array<UserSettingId, 104> 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<UserSettingId> title_writable_settings = {
UserSettingId::XPROFILE_TITLE_SPECIFIC1,
UserSettingId::XPROFILE_TITLE_SPECIFIC2,
UserSettingId::XPROFILE_TITLE_SPECIFIC3};
enum PREFERRED_COLOR_OPTIONS : uint32_t {
PREFERRED_COLOR_NONE,
PREFERRED_COLOR_BLACK,
PREFERRED_COLOR_WHITE,
PREFERRED_COLOR_YELLOW,
PREFERRED_COLOR_ORANGE,
PREFERRED_COLOR_PINK,
PREFERRED_COLOR_RED,
PREFERRED_COLOR_PURPLE,
PREFERRED_COLOR_BLUE,
PREFERRED_COLOR_GREEN,
PREFERRED_COLOR_BROWN,
PREFERRED_COLOR_SILVER
};
using SettingTypes = std::variant<uint32_t, int32_t, float, int64_t, double,
std::u16string, std::vector<uint8_t>>;
class UserSetting {
public:
// Ctor for writing from host
UserSetting(UserSettingId setting_id, SettingTypes setting_data);
// Ctor for writing to GPD
UserSetting(const X_USER_PROFILE_SETTING* profile_setting);
// Ctor for reading from GPD
UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting,
std::span<const uint8_t> extended_data);
uint32_t get_setting_id() const { return static_cast<uint32_t>(setting_id_); }
std::vector<uint8_t> serialize_to_gpd() const;
const X_USER_DATA* get_base_data();
std::span<const uint8_t> get_extended_data() const {
return {extended_data_.data(), extended_data_.size()};
}
X_USER_DATA_TYPE get_setting_type() const { return setting_type_; }
static X_USER_DATA_TYPE get_setting_type(uint32_t setting_id) {
return static_cast<X_USER_DATA_TYPE>(setting_id >> 28);
}
static uint16_t get_setting_max_size(uint32_t setting_id) {
return static_cast<uint16_t>(setting_id >> 16) & kMaxSettingSize;
}
static bool is_setting_valid(uint32_t setting_id) {
return std::find(known_settings.cbegin(), known_settings.cend(),
static_cast<UserSettingId>(setting_id)) !=
known_settings.cend();
}
bool is_valid_setting_type() const {
return setting_type_ >= X_USER_DATA_TYPE::CONTEXT &&
setting_type_ <= X_USER_DATA_TYPE::DATETIME;
}
bool requires_additional_data() const {
return setting_type_ == X_USER_DATA_TYPE::BINARY ||
setting_type_ == X_USER_DATA_TYPE::WSTRING;
}
static bool requires_additional_data(uint32_t setting_id) {
const auto setting_type = get_setting_type(setting_id);
return setting_type == X_USER_DATA_TYPE::BINARY ||
setting_type == X_USER_DATA_TYPE::WSTRING;
}
static size_t get_setting_data_size(const X_USER_PROFILE_SETTING* setting) {
if (requires_additional_data(setting->setting_id)) {
return std::min(get_setting_max_size(setting->setting_id),
static_cast<uint16_t>(setting->data.data.binary.size));
}
return get_setting_max_size(setting->setting_id);
}
private:
UserSettingId setting_id_;
X_USER_DATA_TYPE setting_type_;
X_USER_PROFILE_SETTING_SOURCE setting_source_;
X_USER_DATA user_data_;
uint16_t size_;
uint16_t max_size_;
std::vector<uint8_t> extended_data_;
static bool is_valid_setting(uint32_t title_id,
const UserSettingId setting_id) {
if (title_id != kDashboardID && title_writable_settings.count(setting_id)) {
return true;
}
if (title_id == kDashboardID &&
!title_writable_settings.count(setting_id)) {
return true;
}
return false;
}
static bool is_title_specific(uint32_t setting_id) {
return (setting_id & 0x3F00) == 0x3F00;
}
// bool is_setting_writable()
bool is_title_specific() const {
return is_title_specific(static_cast<uint32_t>(setting_id_));
}
};
} // namespace xam
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_XAM_USER_PROFILE_H_

View File

@ -0,0 +1,700 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/kernel/xam/user_profile.h"
#include <ranges>
#include <sstream>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/user_settings.h"
#include "xenia/kernel/xam/user_tracker.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
DECLARE_int32(user_language);
namespace xe {
namespace kernel {
namespace xam {
UserTracker::UserTracker() : spa_data_(nullptr) {}
UserTracker::~UserTracker() {}
bool UserTracker::AddUser(uint64_t xuid) {
if (IsUserTracked(xuid)) {
XELOGW("{}: User is already on tracking list!");
return false;
}
tracked_xuids_.insert(xuid);
if (spa_data_) {
AddTitleToPlayedList(xuid);
}
return true;
}
bool UserTracker::RemoveUser(uint64_t xuid) {
if (!IsUserTracked(xuid)) {
XELOGW("{}: User is not on tracking list!");
return false;
}
tracked_xuids_.erase(xuid);
FlushUserData(xuid);
return true;
}
bool UserTracker::UnlockAchievement(uint64_t xuid, uint32_t achievement_id) {
if (!IsUserTracked(xuid)) {
XELOGW("{}: User is not on tracking list!");
return false;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return false;
}
if (!spa_data_) {
return false;
}
const auto spa_achievement = spa_data_->GetAchievement(achievement_id);
if (!spa_achievement) {
return false;
}
// Update data in profile gpd.
auto title_info = user->dashboard_gpd_.GetTitleInfo(spa_data_->title_id());
if (!title_info) {
return false;
}
// Update title gpd
auto title_gpd = &user->games_gpd_[spa_data_->title_id()];
// Achievement is unlocked, so we need to add achievement icon
title_gpd->AddImage(spa_achievement->image_id,
spa_data_->GetIcon(spa_achievement->image_id));
auto gpd_achievement = title_gpd->GetAchievementEntry(spa_achievement->id);
if (!gpd_achievement) {
return false;
}
title_info->achievements_unlocked++;
title_info->gamerscore_earned += spa_achievement->gamerscore;
const std::string achievement_name = spa_data_->GetStringTableEntry(
spa_data_->default_language(), spa_achievement->label_id);
XELOGI("Player: {} Unlocked Achievement: {}", user->name(),
achievement_name.c_str());
gpd_achievement->flags = gpd_achievement->flags |
static_cast<uint32_t>(AchievementFlags::kAchieved);
gpd_achievement->unlock_time = Clock::QueryGuestSystemTime();
UpdateSettingValue(xuid, kDashboardID, UserSettingId::XPROFILE_GAMERCARD_CRED,
gpd_achievement->gamerscore);
UpdateSettingValue(xuid, kDashboardID,
UserSettingId::XPROFILE_GAMERCARD_ACHIEVEMENTS_EARNED, 1);
UpdateSettingValue(xuid, spa_data_->title_id(),
UserSettingId::XPROFILE_GAMERCARD_TITLE_CRED_EARNED,
gpd_achievement->gamerscore);
UpdateSettingValue(
xuid, spa_data_->title_id(),
UserSettingId::XPROFILE_GAMERCARD_TITLE_ACHIEVEMENTS_EARNED, 1);
FlushUserData(xuid);
return true;
}
void UserTracker::FlushGpd(const uint64_t xuid, const uint32_t id) {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
GpdInfo* gpd = GetGpd(user, id);
if (!gpd) {
return;
}
if (!gpd->RequiresFlush()) {
return;
}
std::vector<uint8_t> data = gpd->Serialize();
vfs::File* file = nullptr;
vfs::FileAction action;
const auto mounted_path = fmt::format("{:016X}:\\{:08X}.gpd", xuid, id);
// If entry exist then just update it otherwise create a new file
vfs::Entry* entry = kernel_state()->file_system()->ResolvePath(mounted_path);
if (entry) {
const auto result = kernel_state()->file_system()->OpenFile(
nullptr, mounted_path, vfs::FileDisposition::kOpen,
vfs::FileAccess::kGenericAll, false, true, &file, &action);
if (result != X_STATUS_SUCCESS) {
return;
}
size_t written_bytes = 0;
file->WriteSync(data.data(), data.size(), 0, &written_bytes);
file->Destroy();
return;
}
const auto result = kernel_state()->file_system()->OpenFile(
nullptr, mounted_path, vfs::FileDisposition::kCreate,
vfs::FileAccess::kGenericAll, false, true, &file, &action);
if (result != X_STATUS_SUCCESS) {
return;
}
size_t written_bytes = 0;
file->WriteSync(data.data(), data.size(), 0, &written_bytes);
file->Destroy();
}
void UserTracker::FlushUserData(const uint64_t xuid) {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
FlushGpd(xuid, kDashboardID);
if (spa_data_) {
FlushGpd(xuid, spa_data_->title_id());
}
}
void UserTracker::AddTitleToPlayedList() {
if (!spa_data_) {
return;
}
for (const uint64_t xuid : tracked_xuids_) {
AddTitleToPlayedList(xuid);
}
}
void UserTracker::AddTitleToPlayedList(uint64_t xuid) {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
if (!spa_data_) {
return;
}
const uint32_t title_id = spa_data_->title_id();
auto title_gpd = user->games_gpd_.find(title_id);
if (title_gpd == user->games_gpd_.end()) {
user->games_gpd_.emplace(title_id, GpdInfoTitle(title_id));
UpdateTitleGpdFile();
}
if (!spa_data_->include_in_profile()) {
return;
}
const uint64_t current_time = Clock::QueryGuestSystemTime();
auto title_info = user->dashboard_gpd_.GetTitleInfo(title_id);
if (!title_info) {
user->dashboard_gpd_.AddNewTitle(spa_data_);
UpdateSettingValue(xuid, kDashboardID,
UserSettingId::XPROFILE_GAMERCARD_TITLES_PLAYED, 1);
title_info = user->dashboard_gpd_.GetTitleInfo(title_id);
}
// Normally we only need to update last booted time. Everything else is filled
// during creation time OR SPA UPDATE TIME!
title_info->last_played = current_time;
UpdateProfileGpd();
}
// Privates
bool UserTracker::IsUserTracked(uint64_t xuid) const {
return tracked_xuids_.find(xuid) != tracked_xuids_.cend();
}
std::optional<TitleInfo> UserTracker::GetUserTitleInfo(
uint64_t xuid, uint32_t title_id) const {
if (!IsUserTracked(xuid)) {
XELOGW("{}: User is not on tracking list!");
return std::nullopt;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return std::nullopt;
}
const auto title_data = user->dashboard_gpd_.GetTitleInfo(title_id);
if (!title_data) {
return std::nullopt;
}
auto game_gpd = user->games_gpd_.find(title_id);
if (game_gpd == user->games_gpd_.cend()) {
return std::nullopt;
}
TitleInfo info;
info.id = title_data->title_id;
info.achievements_count = title_data->achievements_count;
info.unlocked_achievements_count = title_data->achievements_unlocked;
info.gamerscore_amount = title_data->gamerscore_total;
info.title_earned_gamerscore = title_data->gamerscore_earned;
info.title_name = xe::to_utf8(user->dashboard_gpd_.GetTitleName(title_id));
info.icon = game_gpd->second.GetImage(kXdbfIdTitle);
if ((title_data->last_played >> 56) != 0) {
info.last_played = chrono::WinSystemClock::to_local(
X_ACHIEVEMENT_UNLOCK_TIME(title_data->last_played).to_time_point());
}
return info;
}
std::vector<TitleInfo> UserTracker::GetPlayedTitles(uint64_t xuid) const {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return {};
}
std::vector<TitleInfo> played_titles;
const auto titles_data = user->dashboard_gpd_.GetTitlesInfo();
for (const auto& title_data : titles_data) {
if (!title_data->include_in_enumerator()) {
continue;
}
TitleInfo info;
info.id = title_data->title_id;
info.achievements_count = title_data->achievements_count;
info.unlocked_achievements_count = title_data->achievements_unlocked;
info.gamerscore_amount = title_data->gamerscore_total;
info.title_earned_gamerscore = title_data->gamerscore_earned;
info.title_name =
xe::to_utf8(user->dashboard_gpd_.GetTitleName(title_data->title_id));
if ((title_data->last_played >> 56) != 0) {
info.last_played = chrono::WinSystemClock::to_local(
X_ACHIEVEMENT_UNLOCK_TIME(title_data->last_played).to_time_point());
}
auto game_gpd = user->games_gpd_.find(title_data->title_id);
if (game_gpd != user->games_gpd_.cend()) {
info.icon = game_gpd->second.GetImage(kXdbfIdTitle);
}
played_titles.push_back(info);
}
std::sort(played_titles.begin(), played_titles.end(),
[](const TitleInfo& first, const TitleInfo& second) {
return first.last_played > second.last_played;
});
return played_titles;
}
void UserTracker::UpdateSpaInfo(SpaInfo* spa_info) {
spa_data_ = spa_info;
if (!spa_data_) {
return;
}
UpdateProfileGpd();
UpdateTitleGpdFile();
}
void UserTracker::UpdateTitleGpdFile() {
for (auto& user_xuid : tracked_xuids_) {
auto user = kernel_state()->xam_state()->GetUserProfile(user_xuid);
if (!user) {
continue;
}
auto game_gpd = user->games_gpd_.find(spa_data_->title_id());
if (game_gpd == user->games_gpd_.cend()) {
continue;
}
auto user_language = spa_data_->GetExistingLanguage(
static_cast<XLanguage>(cvars::user_language));
// First add achievements because of lowest ID
for (const auto& entry : spa_data_->GetAchievements()) {
AchievementDetails details(entry, spa_data_, user_language);
game_gpd->second.AddAchievement(&details);
}
// Then add game icon
game_gpd->second.AddImage(kXdbfIdTitle, spa_data_->title_icon());
// At the end add title name entry
game_gpd->second.AddString(kXdbfIdTitle,
xe::to_utf16(spa_data_->title_name()));
// Check if we have icon for every unlocked achievements.
FlushUserData(user_xuid);
}
}
void UserTracker::UpdateProfileGpd() {
for (auto& user_xuid : tracked_xuids_) {
auto user = kernel_state()->xam_state()->GetUserProfile(user_xuid);
if (!user) {
continue;
}
auto title_data = user->dashboard_gpd_.GetTitleInfo(spa_data_->title_id());
if (!title_data) {
continue;
}
const uint32_t achievements_count = spa_data_->achievement_count();
// If achievements count doesn't match then obviously gamerscore won't match
// either
if (title_data->achievements_count < achievements_count) {
auto title_updated_data = *title_data;
title_updated_data.achievements_count = achievements_count;
title_updated_data.gamerscore_total = spa_data_->total_gamerscore();
user->dashboard_gpd_.UpdateTitleInfo(spa_data_->title_id(),
&title_updated_data);
}
FlushUserData(user_xuid);
}
}
std::vector<Achievement> 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<Achievement> 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<const uint8_t> UserTracker::GetAchievementIcon(
uint64_t xuid, uint32_t title_id, uint32_t achievement_id) const {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return {};
}
auto game_gpd = user->games_gpd_.find(title_id);
if (game_gpd == user->games_gpd_.cend()) {
return {};
}
const auto entry = game_gpd->second.GetAchievementEntry(achievement_id);
if (entry == nullptr) {
return {};
}
return game_gpd->second.GetImage(entry->image_id);
}
void UserTracker::AddProperty(const uint64_t xuid, const Property* property) {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
// Find if property already exits
Property* entry = GetProperty(user->xuid(), property->GetPropertyId());
if (entry) {
*entry = *property;
return;
}
user->properties_.push_back(*property);
}
Property* UserTracker::GetProperty(const uint64_t xuid, const AttributeKey id) {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return nullptr;
}
for (auto& entry : user->properties_) {
if (entry.GetPropertyId().value != id.value) {
continue;
}
return &entry;
}
return nullptr;
}
std::optional<UserSetting> UserTracker::GetGpdSetting(
UserProfile* user, uint32_t setting_id) const {
if (!spa_data_) {
// There is no data about current title. Use dashboard info.
auto setting = user->dashboard_gpd_.GetSetting(setting_id);
if (!setting) {
return std::nullopt;
}
return std::make_optional<UserSetting>(
setting, user->dashboard_gpd_.GetSettingData(setting_id));
}
auto game_gpd = user->games_gpd_.find(spa_data_->title_id());
if (game_gpd == user->games_gpd_.cend()) {
return std::nullopt;
}
auto setting = game_gpd->second.GetSetting(setting_id);
if (!setting) {
// Refer to default values
return std::nullopt;
}
return std::make_optional<UserSetting>(
setting, game_gpd->second.GetSettingData(setting_id));
}
std::optional<UserSetting> UserTracker::GetDefaultSetting(
UserProfile* user, uint32_t setting_id) const {
const auto type = UserSetting::get_setting_type(setting_id);
if (type == X_USER_DATA_TYPE::WSTRING) {
return std::make_optional<UserSetting>(
static_cast<UserSettingId>(setting_id), std::u16string());
}
if (type == X_USER_DATA_TYPE::BINARY) {
return std::make_optional<UserSetting>(
static_cast<UserSettingId>(setting_id), std::vector<uint8_t>());
}
if (type == X_USER_DATA_TYPE::FLOAT) {
return std::make_optional<UserSetting>(
static_cast<UserSettingId>(setting_id), 0.0f);
}
if (type == X_USER_DATA_TYPE::DOUBLE) {
return std::make_optional<UserSetting>(
static_cast<UserSettingId>(setting_id), 0.0);
}
if (type == X_USER_DATA_TYPE::DATETIME || type == X_USER_DATA_TYPE::INT64) {
return std::make_optional<UserSetting>(
static_cast<UserSettingId>(setting_id), static_cast<int64_t>(0));
}
return std::make_optional<UserSetting>(static_cast<UserSettingId>(setting_id),
0);
}
bool UserTracker::GetUserSetting(uint64_t xuid, uint32_t setting_id,
X_USER_PROFILE_SETTING* setting_ptr,
uint32_t& extended_data_address) const {
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return false;
}
auto gpd_setting = GetGpdSetting(user, setting_id);
// We have entry in gpd so use it
if (gpd_setting) {
setting_ptr->setting_id = setting_id;
setting_ptr->source = X_USER_PROFILE_SETTING_SOURCE::TITLE;
const auto base_data = gpd_setting->get_base_data();
memcpy(&setting_ptr->data, base_data, sizeof(X_USER_DATA));
// Check how to deal with writing additional data
if (gpd_setting->requires_additional_data()) {
const auto extended_data = gpd_setting->get_extended_data();
setting_ptr->data.data.binary.size =
static_cast<uint32_t>(extended_data.size_bytes());
setting_ptr->data.data.binary.ptr = extended_data_address;
memcpy(kernel_memory()->TranslateVirtual(extended_data_address),
extended_data.data(), extended_data.size_bytes());
extended_data_address +=
static_cast<uint32_t>(extended_data.size_bytes());
}
return true;
}
// Get setting from defaults
auto setting = GetDefaultSetting(user, setting_id);
if (setting) {
setting_ptr->setting_id = setting_id;
setting_ptr->source = X_USER_PROFILE_SETTING_SOURCE::DEFAULT;
const auto base_data = setting->get_base_data();
memcpy(&setting_ptr->data, base_data, sizeof(X_USER_DATA));
// There is no additional data for default values
if (setting->requires_additional_data()) {
setting_ptr->data.data.binary.size = 0;
setting_ptr->data.data.binary.ptr = extended_data_address;
}
}
return true;
}
void UserTracker::UpdateContext(uint64_t xuid, uint32_t id, uint32_t value) {
if (!IsUserTracked(xuid)) {
return;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
const auto& context_data = spa_data_->GetContext(id);
if (!context_data) {
return;
}
user->contexts_[id] = value > context_data->max_value
? static_cast<uint32_t>(context_data->default_value)
: value;
}
std::optional<uint32_t> UserTracker::GetUserContext(uint64_t xuid,
uint32_t id) const {
if (!IsUserTracked(xuid)) {
return std::nullopt;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return std::nullopt;
}
const auto& context_data = spa_data_->GetContext(id);
if (!context_data) {
return std::nullopt;
}
if (!user->contexts_.count(id)) {
return std::nullopt;
}
return user->contexts_[id];
}
void UserTracker::UpdateSettingValue(uint64_t xuid, uint32_t title_id,
UserSettingId setting_id,
int32_t difference) {
if (!IsUserTracked(xuid)) {
return;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
GpdInfo* info = GetGpd(user, title_id);
if (!info) {
return;
}
auto setting = info->GetSetting(static_cast<uint32_t>(setting_id));
if (!setting) {
UserSetting new_setting(setting_id, difference);
info->UpsertSetting(&new_setting);
return;
}
const int32_t new_value = setting->base_data.s32 + difference;
UserSetting new_setting(setting_id, new_value);
info->UpsertSetting(&new_setting);
}
void UserTracker::UpsertSetting(uint64_t xuid, uint32_t title_id,
const UserSetting* setting) {
if (!IsUserTracked(xuid)) {
return;
}
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
if (!user) {
return;
}
// Sometimes games like to ignore providing expicitly title_id, so we need to
// check it.
if (!title_id) {
title_id = spa_data_->title_id();
}
GpdInfo* info = GetGpd(user, title_id);
if (!info) {
return;
}
info->UpsertSetting(setting);
FlushUserData(xuid);
}
GpdInfo* UserTracker::GetGpd(UserProfile* profile, const uint32_t title_id) {
if (title_id == kDashboardID) {
return &profile->dashboard_gpd_;
}
if (!profile->games_gpd_.count(title_id)) {
return nullptr;
}
return &profile->games_gpd_[title_id];
}
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -0,0 +1,113 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XAM_USER_TRACKER_H_
#define XENIA_KERNEL_XAM_USER_TRACKER_H_
#include <chrono>
#include <set>
#include <span>
#include "xenia/xbox.h"
#include "xenia/kernel/xam/user_settings.h"
namespace xe {
namespace kernel {
namespace xam {
struct TitleInfo {
std::string title_name;
uint32_t id;
uint32_t unlocked_achievements_count;
uint32_t achievements_count;
uint32_t title_earned_gamerscore;
uint32_t gamerscore_amount;
std::chrono::local_time<std::chrono::system_clock::duration> last_played;
std::span<const uint8_t> icon;
bool WasTitlePlayed() const {
return last_played.time_since_epoch().count() != 0;
}
};
class UserTracker {
public:
UserTracker();
~UserTracker();
// UserTracker specific methods
bool AddUser(uint64_t xuid);
bool RemoveUser(uint64_t xuid);
// SPA related methods
void UpdateSpaInfo(SpaInfo* spa_info);
// User related methods
bool UnlockAchievement(uint64_t xuid, uint32_t achievement_id);
// Context
void UpdateContext(uint64_t xuid, uint32_t id, uint32_t value);
std::optional<uint32_t> GetUserContext(uint64_t xuid, uint32_t id) const;
// Property
void AddProperty(const uint64_t xuid, const Property* property);
Property* GetProperty(const uint64_t xuid, const AttributeKey property_id);
// Settings
void UpsertSetting(uint64_t xuid, uint32_t title_id,
const UserSetting* setting);
bool GetUserSetting(uint64_t xuid, uint32_t setting_id,
X_USER_PROFILE_SETTING* setting_ptr,
uint32_t& extended_data_address) const;
// Titles
void AddTitleToPlayedList();
std::vector<TitleInfo> GetPlayedTitles(uint64_t xuid) const;
std::optional<TitleInfo> GetUserTitleInfo(uint64_t xuid,
uint32_t title_id) const;
// Achievements
std::vector<Achievement> GetUserTitleAchievements(uint64_t xuid,
uint32_t title_id) const;
std::span<const uint8_t> GetAchievementIcon(uint64_t xuid, uint32_t title_id,
uint32_t achievement_id) const;
private:
bool IsUserTracked(uint64_t xuid) const;
void UpdateSettingValue(uint64_t xuid, uint32_t title_id,
UserSettingId setting_id, int32_t difference);
std::optional<UserSetting> GetGpdSetting(UserProfile* user,
uint32_t setting_id) const;
std::optional<UserSetting> GetDefaultSetting(UserProfile* user,
uint32_t setting_id) const;
GpdInfo* GetGpd(UserProfile* profile, const uint32_t title_id);
void AddTitleToPlayedList(uint64_t xuid);
void UpdateTitleGpdFile();
void UpdateProfileGpd();
void FlushUserData(const uint64_t xuid);
void FlushGpd(const uint64_t xuid, const uint32_t id);
SpaInfo* spa_data_;
std::set<uint64_t> tracked_xuids_;
};
} // namespace xam
} // namespace kernel
} // namespace xe
#endif

View File

@ -25,7 +25,9 @@ XamState::XamState(Emulator* emulator, KernelState* kernel_state)
content_manager_ =
std::make_unique<ContentManager>(kernel_state, content_root);
profile_manager_ = std::make_unique<ProfileManager>(kernel_state);
user_tracker_ = std::make_unique<UserTracker>();
profile_manager_ =
std::make_unique<ProfileManager>(kernel_state, user_tracker_.get());
achievement_manager_ = std::make_unique<AchievementManager>();
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<SpaInfo>(*info);
spa_info_->Load();
user_tracker_->UpdateSpaInfo(spa_info_.get());
}
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -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<AppManager> app_manager_;
std::unique_ptr<ContentManager> content_manager_;
std::unique_ptr<UserTracker> user_tracker_;
std::unique_ptr<AchievementManager> achievement_manager_;
std::unique_ptr<ProfileManager> profile_manager_;
std::unique_ptr<SpaInfo> spa_info_;
};
} // namespace xam

View File

@ -17,6 +17,7 @@
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/user_tracker.h"
#include "xenia/kernel/xam/xam_content_device.h"
#include "xenia/kernel/xam/xam_private.h"
#include "xenia/ui/imgui_dialog.h"
@ -520,15 +521,6 @@ struct AchievementInfo {
}
};
struct TitleInfo {
std::string title_name;
uint32_t id;
uint32_t unlocked_achievements_count;
uint32_t achievements_count;
uint32_t title_earned_gamerscore;
uint64_t last_played; // Convert from guest to some tm?
};
class GameAchievementsDialog final : public XamDialog {
public:
GameAchievementsDialog(ui::ImGuiDrawer* imgui_drawer,
@ -552,21 +544,16 @@ class GameAchievementsDialog final : public XamDialog {
->achievement_manager()
->GetTitleAchievements(profile_->xuid(), title_info_.id);
const auto title_gpd = kernel_state()->title_xdbf();
if (!title_achievements) {
if (title_achievements.empty()) {
return false;
}
for (const auto& entry : *title_achievements) {
for (const auto& entry : title_achievements) {
AchievementInfo info;
info.id = entry.achievement_id;
info.name =
xe::load_and_swap<std::u16string>(entry.achievement_name.c_str());
info.desc =
xe::load_and_swap<std::u16string>(entry.unlocked_description.c_str());
info.unachieved =
xe::load_and_swap<std::u16string>(entry.locked_description.c_str());
info.name = entry.achievement_name;
info.desc = entry.unlocked_description;
info.unachieved = entry.locked_description;
info.flags = entry.flags;
info.gamerscore = entry.gamerscore;
@ -580,12 +567,16 @@ class GameAchievementsDialog final : public XamDialog {
achievements_info_.insert({info.id, info});
const auto& icon_entry =
title_gpd.GetEntry(util::XdbfSection::kImage, info.image_id);
const auto icon =
kernel_state()
->xam_state()
->achievement_manager()
->GetAchievementIcon(profile_->xuid(), title_info_.id,
entry.achievement_id);
data.insert({info.image_id,
std::make_pair(icon_entry.buffer,
static_cast<uint32_t>(icon_entry.size))});
data.insert(
{info.image_id,
std::make_pair(icon.data(), static_cast<uint32_t>(icon.size()))});
}
achievements_icons_ = imgui_drawer()->LoadIcons(data);
@ -659,7 +650,7 @@ class GameAchievementsDialog final : public XamDialog {
achievement_entry.unlock_time)
.c_str());
} else {
ImGui::TextUnformatted(fmt::format("Unlocked: Locally").c_str());
ImGui::TextUnformatted(fmt::format("Unlocked: Offline").c_str());
}
}
@ -754,40 +745,18 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
const UserProfile* profile) {
info_.clear();
// TODO(Gliniak): This code should be adjusted for GPD support. Instead of
// using whole profile it should only take vector of gpd entries. Ideally
// remapped to another struct.
if (kernel_state()->emulator()->is_title_open()) {
const auto xdbf = kernel_state()->title_xdbf();
xe::ui::IconsData data;
if (!xdbf.is_valid()) {
return;
info_ = kernel_state()->xam_state()->user_tracker()->GetPlayedTitles(
profile->xuid());
for (const auto& title_info : info_) {
if (!title_info.icon.empty()) {
data[title_info.id] = {title_info.icon.data(),
static_cast<uint32_t>(title_info.icon.size())};
}
const auto title_summary_info =
kernel_state()->achievement_manager()->GetTitleAchievementsInfo(
profile->xuid(), kernel_state()->title_id());
if (!title_summary_info) {
return;
}
TitleInfo game;
game.id = kernel_state()->title_id();
game.title_name = xdbf.title();
game.title_earned_gamerscore = title_summary_info->gamerscore;
game.unlocked_achievements_count =
title_summary_info->unlocked_achievements_count;
game.achievements_count = title_summary_info->achievements_count;
game.last_played = 0;
xe::ui::IconsData data;
const auto& image_data = xdbf.icon();
data[game.id] = {image_data.buffer, (uint32_t)image_data.size};
title_icon = imgui_drawer->LoadIcons(data);
info_.insert({game.id, game});
}
title_icon = imgui_drawer->LoadIcons(data);
}
void DrawTitleEntry(ImGuiIO& io, const TitleInfo& entry) {
@ -816,15 +785,23 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
ImGui::SetCursorPosY(start_position.y + default_image_icon_size.y -
ImGui::GetTextLineHeight());
// TODO(Gliniak): For now I left hardcoded now, but in the future it must be
// changed to include last time of boot.
ImGui::TextUnformatted(fmt::format("Last played: {}", "Now").c_str());
if (entry.WasTitlePlayed()) {
ImGui::TextUnformatted(
fmt::format("Last played: {:%Y-%m-%d %H:%M}", entry.last_played)
.c_str());
} else {
ImGui::TextUnformatted("Last played: Unknown");
}
const ImVec2 end_draw_position =
ImVec2(ImGui::GetCursorPos().x - start_position.x,
ImGui::GetCursorPos().y - start_position.y);
ImGui::SetCursorPos(start_position);
if (ImGui::Selectable("##Selectable", false,
ImGuiSelectableFlags_SpanAllColumns,
ImGui::GetContentRegionAvail())) {
if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(),
false, ImGuiSelectableFlags_SpanAllColumns,
end_draw_position)) {
new GameAchievementsDialog(imgui_drawer(), next_window_position, &entry,
profile_);
}
@ -851,7 +828,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
if (!info_.empty()) {
if (ImGui::BeginTable("", 2,
ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) {
for (const auto& [_, entry] : info_) {
for (const auto& entry : info_) {
ImGui::TableNextRow(0, default_image_icon_size.y);
DrawTitleEntry(io, entry);
}
@ -888,7 +865,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
const UserProfile* profile_;
std::map<uint32_t, std::unique_ptr<ui::ImmediateTexture>> title_icon;
std::map<uint32_t, TitleInfo> info_;
std::vector<TitleInfo> info_;
};
static dword_result_t XamShowMessageBoxUi(
@ -1922,20 +1899,20 @@ dword_result_t XamShowAchievementsUI_entry(dword_t user_index,
return X_ERROR_NO_SUCH_USER;
}
if (!kernel_state()->title_xdbf().is_valid()) {
return X_ERROR_FUNCTION_FAILED;
}
const auto info =
kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo(
user->xuid(), kernel_state()->xam_state()->spa_info()->title_id());
TitleInfo info = {};
info.id = kernel_state()->title_id();
info.title_name = kernel_state()->title_xdbf().title();
if (!info) {
return X_ERROR_NO_SUCH_USER;
}
ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer();
auto close = [](GameAchievementsDialog* dialog) -> void {};
return xeXamDispatchDialogAsync<GameAchievementsDialog>(
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);

View File

@ -7,14 +7,13 @@
******************************************************************************
*/
#include <cstring>
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/string_util.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/user_profile.h"
#include "xenia/kernel/xam/user_settings.h"
#include "xenia/kernel/xam/xam_private.h"
#include "xenia/kernel/xenumerator.h"
#include "xenia/kernel/xthread.h"
@ -208,21 +207,6 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
be<uint32_t>* 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<uint64_t>(xuids[0]) == user_profile->xuid());
}
}
assert_zero(unk); // probably flags
// must have at least 1 to 32 settings
@ -308,13 +292,12 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
bool any_missing = false;
for (uint32_t i = 0; i < setting_count; ++i) {
auto setting_id = static_cast<uint32_t>(setting_ids[i]);
auto setting = user_profile->GetSetting(setting_id);
if (!setting) {
any_missing = true;
if (!UserSetting::is_setting_valid(setting_id)) {
XELOGE(
"xeXamUserReadProfileSettingsEx requested unimplemented setting "
"{:08X}",
setting_id);
any_missing = true;
}
}
if (any_missing) {
@ -334,29 +317,24 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
out_header->settings_ptr =
kernel_state()->memory()->HostToGuestVirtual(out_setting);
DataByteStream out_stream(
kernel_state()->memory()->HostToGuestVirtual(buffer), buffer, buffer_size,
needed_header_size);
uint32_t additional_data_buffer_ptr =
out_header->settings_ptr +
(setting_count * sizeof(X_USER_PROFILE_SETTING));
for (uint32_t n = 0; n < setting_count; ++n) {
uint32_t setting_id = setting_ids[n];
auto setting = user_profile->GetSetting(setting_id);
std::memset(out_setting, 0, sizeof(X_USER_PROFILE_SETTING));
out_setting->from =
!setting ? 0 : static_cast<uint32_t>(setting->GetSettingSource());
auto setting = kernel_state()->xam_state()->user_tracker()->GetUserSetting(
user_profile->xuid(), setting_id, out_setting,
additional_data_buffer_ptr);
if (xuids) {
out_setting->xuid = user_profile->xuid();
} else {
out_setting->xuid = -1;
out_setting->user_index = user_index;
}
out_setting->setting_id = setting_id;
if (setting) {
out_setting->data.type = static_cast<X_USER_DATA_TYPE>(
setting->GetSettingHeader()->setting_type.value);
setting->GetSettingData()->Append(&out_setting->data, &out_stream);
}
++out_setting;
}
@ -412,53 +390,14 @@ dword_result_t XamUserWriteProfileSettings_entry(
}
for (uint32_t n = 0; n < setting_count; ++n) {
const X_USER_PROFILE_SETTING& setting = settings[n];
const UserSetting setting = UserSetting(&settings[n]);
auto setting_type = static_cast<X_USER_DATA_TYPE>(setting.data.type);
if (setting_type == X_USER_DATA_TYPE::UNSET) {
if (!setting.is_valid_setting_type()) {
continue;
}
XELOGD(
"XamUserWriteProfileSettings: setting index [{}]:"
" from={} setting_id={:08X} data.type={}",
n, (uint32_t)setting.from, (uint32_t)setting.setting_id,
static_cast<uint32_t>(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<uint8_t> 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<UserSetting>(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<uint32_t>(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<uint32_t>(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<std::u16string>(entry.achievement_name.c_str()),
xe::load_and_swap<std::u16string>(entry.unlocked_description.c_str()),
xe::load_and_swap<std::u16string>(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);
}

View File

@ -0,0 +1,312 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2024 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/kernel/xam/xdbf/gpd_info.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/user_settings.h"
#include <map>
#include <ranges>
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<uint8_t> buffer)
: XdbfFile({buffer.data(), buffer.size()}), title_id_(title_id) {}
std::span<const uint8_t> GpdInfo::GetImage(uint32_t id) const {
const Entry* entry = GetEntry(static_cast<uint16_t>(GpdSection::kImage), id);
if (!entry) {
return {};
}
return {entry->data.data(), entry->data.size()};
}
void GpdInfo::AddImage(uint32_t id, std::span<const uint8_t> image_data) {
Entry* entry = GetEntry(static_cast<uint16_t>(GpdSection::kImage), id);
if (entry || !image_data.size()) {
return;
}
Entry new_entry(id, static_cast<uint16_t>(GpdSection::kImage),
static_cast<uint32_t>(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<uint16_t>(GpdSection::kSetting), id);
if (!entry) {
return nullptr;
}
return reinterpret_cast<X_XDBF_GPD_SETTING_HEADER*>(entry->data.data());
}
std::span<const uint8_t> GpdInfo::GetSettingData(uint32_t id) {
auto* entry = GetSetting(id);
if (!entry) {
return {};
}
if (entry->setting_type != X_USER_DATA_TYPE::BINARY &&
entry->setting_type != X_USER_DATA_TYPE::WSTRING) {
return {};
}
const uint32_t size = entry->base_data.size;
const uint8_t* data_ptr = reinterpret_cast<uint8_t*>(entry + 1);
return {data_ptr, size};
}
void GpdInfo::UpsertSetting(const UserSetting* setting_data) {
const auto serialized_data = setting_data->serialize_to_gpd();
Entry new_entry(setting_data->get_setting_id(),
static_cast<uint16_t>(GpdSection::kSetting),
static_cast<uint32_t>(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<uint16_t>(GpdSection::kString), id);
if (!entry) {
return {};
}
return string_util::read_u16string_and_swap(
reinterpret_cast<const char16_t*>(entry->data.data()));
}
void GpdInfo::AddString(uint32_t id, std::u16string string_data) {
Entry* entry = GetEntry(static_cast<uint16_t>(GpdSection::kString), id);
if (entry != nullptr) {
return;
}
const uint32_t entry_size =
static_cast<uint32_t>(string_util::size_in_bytes(string_data));
Entry new_entry(id, static_cast<uint16_t>(GpdSection::kString), entry_size);
string_util::copy_and_swap_truncating(
reinterpret_cast<char16_t*>(new_entry.data.data()), string_data,
string_data.length() + 1);
UpsertEntry(&new_entry);
}
std::vector<uint8_t> 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<uint8_t> 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<const Entry*> 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<uint16_t>(section);
}
void GpdInfo::UpsertEntry(Entry* updated_entry) {
auto entry = GetEntry(updated_entry->info.section, updated_entry->info.id);
requires_flush_ = true;
if (entry) {
DeleteEntry(entry);
}
InsertEntry(updated_entry);
}
uint32_t GpdInfo::FindFreeLocation(const uint32_t entry_size) {
assert_false(free_entries_.empty());
uint32_t offset = free_entries_.back().offset;
auto itr = std::find_if(
free_entries_.begin(), free_entries_.end(),
[entry_size](XdbfFileLoc entry) { return entry.size == entry_size; });
// We have exact match, so just get offset and remove entry
if (itr != free_entries_.cend()) {
offset = itr->offset;
header_.free_used--;
free_entries_.erase(itr);
return offset;
}
// Check for any entry that matches size.
itr = std::find_if(
free_entries_.begin(), free_entries_.end(),
[entry_size](XdbfFileLoc entry) { return entry.size > entry_size; });
// There is an requirement that there is always at least one entry, so no need
// to check for valid entry.
offset = itr->offset;
itr->offset += entry_size;
itr->size -= entry_size;
return offset;
}
void GpdInfo::InsertEntry(Entry* entry) {
ResizeEntryTable();
entry->info.offset = FindFreeLocation(entry->info.size);
entries_.push_back(*entry);
header_.entry_used++;
}
void GpdInfo::DeleteEntry(const Entry* entry) {
// Don't really remove entry. Just remove entry in the entry table.
MarkSpaceAsFree(entry->info.offset, entry->info.size);
auto itr =
std::find_if(entries_.begin(), entries_.end(), [entry](Entry first) {
return entry->info.section == first.info.section &&
first.info.id == entry->info.id;
});
if (itr != entries_.end()) {
entries_.erase(itr);
}
header_.entry_used--;
}
std::vector<const Entry*> GpdInfo::GetSortedEntries() const {
std::vector<const Entry*> 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

View File

@ -0,0 +1,157 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XAM_XDBF_GPD_INFO_H_
#define XENIA_KERNEL_XAM_XDBF_GPD_INFO_H_
#include "xenia/kernel/util/xuserdata.h"
#include "xenia/kernel/xam/xdbf/xdbf_io.h"
#include <span>
#include <string>
#include <vector>
#include "xenia/xbox.h"
namespace xe {
namespace kernel {
namespace xam {
class UserSetting;
enum class GpdSection : uint16_t {
kAchievement = 0x1, // TitleGpd exclusive
kImage = 0x2,
kSetting = 0x3,
kTitle = 0x4, // Dashboard Gpd exclusive
kString = 0x5,
kProtectedAchievement = 0x6, // GFWL only
};
struct X_XDBF_AVATARAWARDS_COUNTER {
uint8_t earned;
uint8_t possible;
};
static_assert_size(X_XDBF_AVATARAWARDS_COUNTER, 2);
#pragma pack(push, 1)
struct X_XDBF_GPD_ACHIEVEMENT {
xe::be<uint32_t> magic;
xe::be<uint32_t> id;
xe::be<uint32_t> image_id;
xe::be<uint32_t> gamerscore;
xe::be<uint32_t> flags;
xe::be<uint64_t> unlock_time;
// wchar_t* title;
// wchar_t* description;
// wchar_t* unlocked_description;
};
static_assert_size(X_XDBF_GPD_ACHIEVEMENT, 0x1C);
struct X_XDBF_GPD_TITLE_PLAYED {
xe::be<uint32_t> title_id;
xe::be<uint32_t> achievements_count;
xe::be<uint32_t> achievements_unlocked;
xe::be<uint32_t> gamerscore_total;
xe::be<uint32_t> gamerscore_earned;
xe::be<uint16_t> 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<uint32_t>
flags; // 1 - Offline unlocked, must be synced. 2 - Achievement Unlocked.
// Image missing. 0x10 - Avatar unlocked. Avatar missing.
xe::be<uint64_t> last_played;
// xe::be<char16_t> 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<uint32_t> setting_id;
xe::be<uint32_t> unknown_1;
X_USER_DATA_TYPE setting_type;
char unknown[7];
union {
// Size is used only for types: WSTRING, BINARY
xe::be<uint32_t> size;
// Raw values that can be written. They do not need to be serialized.
xe::be<int32_t> s32;
xe::be<int64_t> s64;
xe::be<uint32_t> u32;
xe::be<double> f64;
xe::be<float> f32;
} base_data;
bool RequiresBuffer() const {
return setting_type == X_USER_DATA_TYPE::BINARY ||
setting_type == X_USER_DATA_TYPE::WSTRING;
}
};
static_assert_size(X_XDBF_GPD_SETTING_HEADER, 0x18);
#pragma pack(pop)
class GpdInfo : public XdbfFile {
public:
GpdInfo();
GpdInfo(const uint32_t title_id);
GpdInfo(const uint32_t title_id, const std::vector<uint8_t> buffer);
bool RequiresFlush() const { return requires_flush_; }
// Normally GPD ALWAYS contains one free entry that indicates EOF
bool IsValid() const { return !free_entries_.empty(); }
// r/w image, setting, string.
std::span<const uint8_t> GetImage(uint32_t id) const;
void AddImage(uint32_t id, std::span<const uint8_t> image_data);
X_XDBF_GPD_SETTING_HEADER* GetSetting(uint32_t id);
std::span<const uint8_t> 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<uint8_t> Serialize() const;
protected:
bool requires_flush_ = false;
static bool IsSyncEntry(const Entry* const entry);
static bool IsEntryOfSection(const Entry* const entry,
const GpdSection section);
void UpsertEntry(Entry* entry);
uint32_t FindFreeLocation(const uint32_t entry_size);
private:
static constexpr uint32_t base_entry_count = 512;
uint32_t title_id_ = 0;
void InsertEntry(Entry* entry);
void DeleteEntry(const Entry* entry);
std::vector<const Entry*> 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_

View File

@ -0,0 +1,115 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2024 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/kernel/xam/xdbf/gpd_info_profile.h"
#include "xenia/base/string_util.h"
#include <ranges>
namespace xe {
namespace kernel {
namespace xam {
const std::vector<const X_XDBF_GPD_TITLE_PLAYED*>
GpdInfoProfile::GetTitlesInfo() const {
std::vector<const X_XDBF_GPD_TITLE_PLAYED*> 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<const X_XDBF_GPD_TITLE_PLAYED*>(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<uint32_t>(entry.info.id) == title_id;
});
if (title.empty()) {
return nullptr;
}
return reinterpret_cast<X_XDBF_GPD_TITLE_PLAYED*>(title.begin()->data.data());
}
std::u16string GpdInfoProfile::GetTitleName(const uint32_t title_id) const {
const Entry* entry =
GetEntry(static_cast<uint16_t>(GpdSection::kTitle), title_id);
if (!entry) {
return std::u16string();
}
return string_util::read_u16string_and_swap(reinterpret_cast<const char16_t*>(
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<uint32_t>(string_util::size_in_bytes(title_name));
Entry entry(title_data->title_id(), static_cast<uint16_t>(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<char16_t*>(entry.data.data() +
sizeof(X_XDBF_GPD_TITLE_PLAYED)),
title_name, title_name.size() + 1);
UpsertEntry(&entry);
}
X_XDBF_GPD_TITLE_PLAYED GpdInfoProfile::FillTitlePlayedData(
const SpaInfo* title_data) const {
X_XDBF_GPD_TITLE_PLAYED title_gpd_data = {};
title_gpd_data.title_id = title_data->title_id();
title_gpd_data.achievements_count = title_data->achievement_count();
title_gpd_data.gamerscore_total = title_data->total_gamerscore();
return title_gpd_data;
}
void GpdInfoProfile::UpdateTitleInfo(const uint32_t title_id,
X_XDBF_GPD_TITLE_PLAYED* title_data) {
auto current_info = GetTitleInfo(title_id);
if (!current_info) {
return;
}
memcpy(current_info, title_data, sizeof(X_XDBF_GPD_TITLE_PLAYED));
requires_flush_ = true;
}
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -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 <string>
#include <vector>
#include "xenia/xbox.h"
namespace xe {
namespace kernel {
namespace xam {
class GpdInfoProfile : public GpdInfo {
public:
GpdInfoProfile() : GpdInfo(0xFFFE07D1) {};
GpdInfoProfile(const std::vector<uint8_t> 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<const X_XDBF_GPD_TITLE_PLAYED*> 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_

View File

@ -0,0 +1,152 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2024 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/kernel/xam/xdbf/gpd_info_title.h"
namespace xe {
namespace kernel {
namespace xam {
X_XDBF_GPD_ACHIEVEMENT* GpdInfoTitle::GetAchievementEntry(const uint32_t id) {
Entry* entry = GetEntry(static_cast<uint16_t>(GpdSection::kAchievement), id);
if (!entry) {
return nullptr;
}
return reinterpret_cast<X_XDBF_GPD_ACHIEVEMENT*>(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<const char16_t*>(++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<const char16_t*>(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<const char16_t*>(
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<uint32_t> GpdInfoTitle::GetAchievementsIds() const {
std::vector<uint32_t> ids;
for (const auto& entry : entries_) {
if (entry.info.section != static_cast<uint16_t>(GpdSection::kAchievement)) {
continue;
}
ids.push_back(static_cast<uint32_t>(entry.info.id));
}
return ids;
}
void GpdInfoTitle::AddAchievement(const AchievementDetails* header) {
Entry* entry =
GetEntry(static_cast<uint16_t>(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<uint32_t>(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<uint16_t>(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<char16_t*>(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<char16_t*>(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<char16_t*>(write_ptr),
header->unachieved,
header->unachieved.length() + 1);
UpsertEntry(&new_entry);
}
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -0,0 +1,55 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_
#define XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_
#include "xenia/kernel/xam/achievement_manager.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
#include "xenia/kernel/xam/xdbf/xdbf_io.h"
#include <string>
#include <vector>
#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<uint8_t> buffer)
: GpdInfo(title_id, buffer) {};
~GpdInfoTitle() {};
std::vector<uint32_t> GetAchievementsIds() const;
void AddAchievement(const AchievementDetails* header);
X_XDBF_GPD_ACHIEVEMENT* GetAchievementEntry(const uint32_t id);
std::u16string GetAchievementTitle(const uint32_t id);
std::u16string GetAchievementDescription(const uint32_t id);
std::u16string GetAchievementUnachievedDescription(const uint32_t id);
private:
const char16_t* GetAchievementTitlePtr(const uint32_t id);
const char16_t* GetAchievementDescriptionPtr(const uint32_t id);
const char16_t* GetAchievementUnachievedDescriptionPtr(const uint32_t id);
};
} // namespace xam
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_XAM_XDBF_GPD_INFO_TITLE_H_

View File

@ -0,0 +1,308 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2024 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/kernel/xam/xdbf/spa_info.h"
#include <map>
namespace xe {
namespace kernel {
namespace xam {
SpaInfo::SpaInfo(const std::span<uint8_t> 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<uint64_t>(XLanguage::kMaxLanguages); language++) {
auto section =
GetEntry(static_cast<uint16_t>(SpaSection::kStringTable), language);
if (!section) {
continue;
}
auto section_header =
reinterpret_cast<const XdbfSectionHeaderEx*>(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<const XdbfStringTableEntry*>(ptr);
std::string string_data = std::string(
reinterpret_cast<const char*>(ptr + sizeof(XdbfStringTableEntry)),
entry->string_length);
strings[entry->id] = string_data;
ptr += entry->string_length + sizeof(XdbfStringTableEntry);
}
language_strings_[static_cast<XLanguage>(language)] = strings;
}
}
void SpaInfo::LoadAchievements() {
auto section =
GetEntry(static_cast<uint16_t>(SpaSection::kMetadata), kXdbfIdXach);
if (!section) {
return;
}
auto section_header =
reinterpret_cast<const XdbfSectionHeaderEx*>(section->data.data());
assert_true(section_header->magic == kXdbfSignatureXach);
assert_true(section_header->version == 1);
AchievementTableEntry* ptr = reinterpret_cast<AchievementTableEntry*>(
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<uint16_t>(SpaSection::kMetadata), kXdbfIdXprp);
if (!property_table) {
return;
}
auto xprp_head =
reinterpret_cast<const XdbfSectionHeader*>(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<const uint16_t*>(ptr));
ptr += sizeof(uint16_t);
for (uint16_t i = 0; i < properties_count; i++) {
auto entry = reinterpret_cast<const XdbfPropertyTableEntry*>(ptr);
ptr += sizeof(XdbfPropertyTableEntry);
properties_.push_back(entry);
}
}
void SpaInfo::LoadContexts() {
auto contexts_table =
GetEntry(static_cast<uint16_t>(SpaSection::kMetadata), kXdbfIdXctx);
if (!contexts_table) {
return;
}
auto xcxt_head =
reinterpret_cast<const XdbfSectionHeader*>(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<const uint32_t*>(ptr));
ptr += sizeof(uint32_t);
for (uint32_t i = 0; i < contexts_count; i++) {
auto entry = reinterpret_cast<const XdbfContextTableEntry*>(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<uint16_t>(SpaSection::kMetadata), kXdbfIdXsrc);
if (!xlast_table) {
return nullptr;
}
auto xlast_head =
reinterpret_cast<const XdbfSectionHeader*>(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<const uint32_t*>(ptr));
ptr += sizeof(uint32_t) + filename_length;
decompressed_size = xe::byte_swap(*reinterpret_cast<const uint32_t*>(ptr));
ptr += sizeof(uint32_t);
compressed_size = xe::byte_swap(*reinterpret_cast<const uint32_t*>(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<const uint8_t> SpaInfo::title_icon() const {
return GetIcon(kXdbfIdTitle);
}
XLanguage SpaInfo::default_language() const {
auto block =
GetEntry(static_cast<uint16_t>(SpaSection::kMetadata), kXdbfIdXstc);
if (!block) {
return XLanguage::kEnglish;
}
auto xstc = reinterpret_cast<const XdbfXstc*>(block->data.data());
return static_cast<XLanguage>(static_cast<uint32_t>(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<uint32_t>(TitleFlags::kAlwaysIncludeInProfile)) {
return true;
}
if (title_header_.flags &
static_cast<uint32_t>(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<uint16_t>(SpaSection::kMetadata), kXdbfIdXthd);
if (!section) {
return;
}
auto section_header =
reinterpret_cast<const XdbfSectionHeader*>(section->data.data());
assert_true(section_header->magic == kXdbfSignatureXthd);
assert_true(section_header->version == 1);
TitleHeaderData* ptr = reinterpret_cast<TitleHeaderData*>(
section->data.data() + sizeof(XdbfSectionHeader));
title_header_ = *ptr;
}
std::string SpaInfo::GetStringTableEntry(XLanguage language,
uint16_t string_id) const {
auto language_table = language_strings_.find(language);
if (language_table == language_strings_.cend()) {
return "";
}
auto entry = language_table->second.find(string_id);
if (entry == language_table->second.cend()) {
return "";
}
return entry->second;
}
const AchievementTableEntry* SpaInfo::GetAchievement(uint32_t id) {
for (const auto& entry : achievements_) {
if (entry->id != id) {
continue;
}
return entry;
}
return nullptr;
}
const XdbfContextTableEntry* SpaInfo::GetContext(uint32_t id) {
for (const auto& entry : contexts_) {
if (entry->id != id) {
continue;
}
return entry;
}
return nullptr;
}
const XdbfPropertyTableEntry* SpaInfo::GetProperty(uint32_t id) {
for (const auto& entry : properties_) {
if (entry->id != id) {
continue;
}
return entry;
}
return nullptr;
}
std::span<const uint8_t> SpaInfo::GetIcon(uint64_t id) const {
auto entry = GetEntry(static_cast<uint16_t>(SpaSection::kImage), id);
if (!entry) {
return {};
}
return {entry->data.data(), entry->data.size()};
}
} // namespace xam
} // namespace kernel
} // namespace xe

View File

@ -0,0 +1,219 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_
#define XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_
#include "xenia/kernel/xam/xdbf/xdbf_io.h"
#include <numeric>
#include <string>
#include <vector>
#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<uint32_t> title_id;
xe::be<TitleType> title_type;
xe::be<uint16_t> major;
xe::be<uint16_t> minor;
xe::be<uint16_t> build;
xe::be<uint16_t> revision;
xe::be<uint32_t> flags;
xe::be<uint32_t> padding_1;
xe::be<uint32_t> padding_2;
xe::be<uint32_t> padding_3;
};
static_assert_size(TitleHeaderData, 32);
struct StatsViewTableEntry {
xe::be<uint32_t> id;
xe::be<uint32_t> flags;
xe::be<uint16_t> shared_index;
xe::be<uint16_t> string_id;
xe::be<uint32_t> unused;
};
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<uint16_t> attribute_id;
xe::be<uint16_t> string_id;
xe::be<uint16_t> aggregation_type;
xe::be<uint8_t> ordinal;
xe::be<uint8_t> field_type;
xe::be<uint32_t> format_type;
xe::be<uint32_t> unused_1;
xe::be<uint32_t> unused_2;
};
static_assert_size(ViewFieldEntry, 0x20);
struct SharedViewMetaTableEntry {
xe::be<uint16_t> column_count;
xe::be<uint16_t> row_count;
xe::be<uint32_t> unused_1;
xe::be<uint32_t> unused_2;
};
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;
PropertyBag property_bag;
};
struct ViewTable {
StatsViewTableEntry view_entry;
SharedView shared_view;
};
struct AchievementTableEntry {
xe::be<uint16_t> id;
xe::be<uint16_t> label_id;
xe::be<uint16_t> description_id;
xe::be<uint16_t> unachieved_id;
xe::be<uint32_t> image_id;
xe::be<uint16_t> gamerscore;
xe::be<uint16_t> unkE;
xe::be<uint32_t> flags;
xe::be<uint32_t> unk14;
xe::be<uint32_t> unk18;
xe::be<uint32_t> unk1C;
xe::be<uint32_t> unk20;
};
static_assert_size(AchievementTableEntry, 0x24);
#pragma pack(pop)
class SpaInfo : public XdbfFile {
public:
SpaInfo(const std::span<uint8_t> 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<const uint8_t> title_icon() const;
std::span<const uint8_t> 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<uint32_t>(achievements_.size());
}
const AchievementTableEntry* GetAchievement(uint32_t id);
std::vector<const AchievementTableEntry*> GetAchievements() const {
return achievements_;
}
std::vector<const XdbfContextTableEntry*> GetContexts() const {
return contexts_;
}
std::vector<const XdbfPropertyTableEntry*> 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<const AchievementTableEntry*> achievements_;
std::vector<const XdbfContextTableEntry*> contexts_;
std::vector<const XdbfPropertyTableEntry*> properties_;
typedef std::map<uint16_t, std::string> XdbfLanguageStrings;
std::map<XLanguage, XdbfLanguageStrings> language_strings_;
void LoadTitleInformation();
void LoadAchievements();
void LoadLanguageData();
void LoadContexts();
void LoadProperties();
};
} // namespace xam
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_XAM_XDBF_SPA_INFO_H_

View File

@ -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<const uint8_t> buffer) {
if (buffer.size() <= sizeof(XdbfHeader)) {
return;
}
const uint8_t* ptr = buffer.data();
if (!LoadHeader(reinterpret_cast<const XdbfHeader*>(ptr))) {
return;
}
ptr += sizeof(XdbfHeader);
const XdbfFileLoc* free_ptr = reinterpret_cast<const XdbfFileLoc*>(
ptr + (sizeof(XdbfEntry) * header_.entry_count));
const uint8_t* data_ptr = reinterpret_cast<const uint8_t*>(free_ptr) +
(sizeof(XdbfFileLoc) * header_.free_count);
LoadEntries(reinterpret_cast<const XdbfEntry*>(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

View File

@ -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 <span>
#include <string>
#include <vector>
#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<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> entry_count;
xe::be<uint32_t> entry_used;
xe::be<uint32_t> free_count;
xe::be<uint32_t> free_used;
};
static_assert_size(XdbfHeader, 24);
struct XdbfEntry {
xe::be<uint16_t> section;
xe::be<uint64_t> id;
xe::be<uint32_t> offset;
xe::be<uint32_t> size;
};
static_assert_size(XdbfEntry, 18);
struct XdbfFileLoc {
xe::be<uint32_t> offset;
xe::be<uint32_t> size;
};
static_assert_size(XdbfFileLoc, 8);
struct XdbfXstc {
xe::be<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> size;
xe::be<uint32_t> default_language;
};
static_assert_size(XdbfXstc, 16);
struct XdbfSectionHeader {
xe::be<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> size;
};
static_assert_size(XdbfSectionHeader, 12);
struct XdbfSectionHeaderEx {
xe::be<uint32_t> magic;
xe::be<uint32_t> version;
xe::be<uint32_t> size;
xe::be<uint16_t> count;
};
static_assert_size(XdbfSectionHeaderEx, 14);
struct XdbfStringTableEntry {
xe::be<uint16_t> id;
xe::be<uint16_t> string_length;
};
static_assert_size(XdbfStringTableEntry, 4);
struct XdbfContextTableEntry {
xe::be<uint32_t> id;
xe::be<uint16_t> unk1;
xe::be<uint16_t> string_id;
xe::be<uint32_t> max_value;
xe::be<uint32_t> default_value;
};
static_assert_size(XdbfContextTableEntry, 16);
struct XdbfPropertyTableEntry {
xe::be<uint32_t> id;
xe::be<uint16_t> string_id;
xe::be<uint16_t> 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<uint8_t> data;
};
// Wraps an XDBF (XboxDataBaseFormat) in-memory database.
// https://free60project.github.io/wiki/XDBF.html
class XdbfFile {
public:
XdbfFile() {};
XdbfFile(const std::span<const uint8_t> buffer);
const Entry* const GetEntry(uint16_t section, uint64_t id) const;
protected:
XdbfHeader header_ = {};
std::vector<Entry> entries_ = {};
std::vector<XdbfFileLoc> 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_

View File

@ -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<AchievementDetails> items_;
std::vector<xam::AchievementDetails> items_;
size_t current_item_ = 0;
};

View File

@ -249,6 +249,9 @@ std::map<uint32_t, std::unique_ptr<ImmediateTexture>> ImGuiDrawer::LoadIcons(
stbi_load_from_memory(icon.second.first, icon.second.second, &width,
&height, &channels, STBI_rgb_alpha);
if (!image_data) {
continue;
}
icons_[icon.first] = (immediate_drawer_->CreateTexture(
width, height, ImmediateTextureFilter::kLinear, true,
reinterpret_cast<uint8_t*>(image_data)));