[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:
parent
b3d345610a
commit
a7c7a3711d
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()); }
|
||||
|
|
|
@ -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(); }
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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_;
|
||||
};
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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_
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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_
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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)));
|
||||
|
|
Loading…
Reference in New Issue