[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
ccf7adf015
commit
1110cdd372
|
@ -1799,10 +1799,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,24 @@ 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.insert(data.begin(), icon.begin(), icon.end());
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -76,17 +77,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 +94,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 +114,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,31 +133,29 @@ 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;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> GameInfoDatabase::GetMatchmakingAttributes(
|
||||
const uint32_t id) const {
|
||||
// TODO(Gliniak): Implement when we will fully understand how to read it from
|
||||
// SPA.
|
||||
std::vector<uint32_t> result;
|
||||
|
||||
const auto xdbf_matchmaking_data = xdbf_gamedata_->GetMatchCollection();
|
||||
|
||||
result.insert(result.end(), xdbf_matchmaking_data.contexts.cbegin(),
|
||||
xdbf_matchmaking_data.contexts.cend());
|
||||
result.insert(result.end(), xdbf_matchmaking_data.properties.cbegin(),
|
||||
xdbf_matchmaking_data.properties.cend());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -240,9 +241,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 +257,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 +273,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_;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Xenia Canary. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/kernel/util/property.h"
|
||||
#include "xenia/base/logging.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
Property::Property(uint32_t property_id, uint32_t value_size,
|
||||
uint8_t* value_ptr) {
|
||||
property_id_.value = property_id;
|
||||
data_type_ = static_cast<X_USER_DATA_TYPE>(property_id_.type);
|
||||
|
||||
value_size_ = value_size;
|
||||
value_.resize(value_size);
|
||||
|
||||
if (value_ptr != 0) {
|
||||
memcpy(value_.data(), value_ptr, value_size);
|
||||
} else {
|
||||
XELOGW("{} Ctor: provided value_ptr is nullptr!", __func__);
|
||||
}
|
||||
}
|
||||
|
||||
Property::Property(const uint8_t* serialized_data, size_t data_size) {
|
||||
if (data_size < 8) {
|
||||
XELOGW("Property::Property lacks information. Skipping!");
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(&property_id_, serialized_data, sizeof(property_id_));
|
||||
data_type_ = static_cast<X_USER_DATA_TYPE>(property_id_.type);
|
||||
|
||||
memcpy(&value_size_, serialized_data + 4, sizeof(value_size_));
|
||||
|
||||
value_.resize(value_size_);
|
||||
memcpy(value_.data(), serialized_data + 8, value_size_);
|
||||
}
|
||||
|
||||
Property::~Property() {};
|
||||
|
||||
std::vector<uint8_t> Property::Serialize() const {
|
||||
std::vector<uint8_t> serialized_property;
|
||||
serialized_property.resize(sizeof(property_id_) + sizeof(value_size_) +
|
||||
value_.size());
|
||||
|
||||
size_t offset = 0;
|
||||
memcpy(serialized_property.data(), &property_id_, sizeof(property_id_));
|
||||
offset += sizeof(property_id_);
|
||||
|
||||
memcpy(serialized_property.data() + offset, &value_size_,
|
||||
sizeof(value_size_));
|
||||
|
||||
offset += sizeof(value_size_);
|
||||
|
||||
memcpy(serialized_property.data() + offset, value_.data(), value_.size());
|
||||
|
||||
return serialized_property;
|
||||
}
|
||||
|
||||
void Property::Write(Memory* memory, XUSER_PROPERTY* property) const {
|
||||
property->property_id = property_id_.value;
|
||||
property->data.type = data_type_;
|
||||
|
||||
switch (data_type_) {
|
||||
case X_USER_DATA_TYPE::WSTRING:
|
||||
property->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 pointer must be valid at this point!
|
||||
memcpy(memory->TranslateVirtual(property->data.binary.ptr), value_.data(),
|
||||
value_size_);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::INT32:
|
||||
memcpy(reinterpret_cast<uint8_t*>(&property->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_);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::DOUBLE:
|
||||
memcpy(reinterpret_cast<uint8_t*>(&property->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_);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::DATETIME:
|
||||
memcpy(reinterpret_cast<uint8_t*>(&property->data.filetime),
|
||||
value_.data(), value_size_);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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::INT32:
|
||||
return *reinterpret_cast<const uint32_t*>(value_.data());
|
||||
case X_USER_DATA_TYPE::INT64:
|
||||
case X_USER_DATA_TYPE::DATETIME:
|
||||
return *reinterpret_cast<const uint64_t*>(value_.data());
|
||||
case X_USER_DATA_TYPE::DOUBLE:
|
||||
return *reinterpret_cast<const double*>(value_.data());
|
||||
case X_USER_DATA_TYPE::WSTRING:
|
||||
return std::u16string(reinterpret_cast<const char16_t*>(value_.data()));
|
||||
case X_USER_DATA_TYPE::FLOAT:
|
||||
return *reinterpret_cast<const float*>(value_.data());
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value_;
|
||||
}
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -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_
|
|
@ -1,234 +0,0 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_UTIL_XUSERDATA_H_
|
||||
#define XENIA_KERNEL_UTIL_XUSERDATA_H_
|
||||
|
||||
#include "xenia/base/byte_stream.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
union AttributeKey {
|
||||
uint32_t value;
|
||||
struct {
|
||||
uint32_t id : 14;
|
||||
uint32_t unk : 2;
|
||||
uint32_t size : 12;
|
||||
uint32_t type : 4;
|
||||
};
|
||||
};
|
||||
|
||||
enum class X_USER_DATA_TYPE : uint8_t {
|
||||
CONTENT = 0,
|
||||
INT32 = 1,
|
||||
INT64 = 2,
|
||||
DOUBLE = 3,
|
||||
WSTRING = 4,
|
||||
FLOAT = 5,
|
||||
BINARY = 6,
|
||||
DATETIME = 7,
|
||||
UNSET = 0xFF,
|
||||
};
|
||||
|
||||
struct X_USER_DATA {
|
||||
X_USER_DATA_TYPE type;
|
||||
|
||||
union {
|
||||
be<int32_t> s32;
|
||||
be<int64_t> s64;
|
||||
be<uint32_t> u32;
|
||||
be<double> f64;
|
||||
struct {
|
||||
be<uint32_t> size;
|
||||
be<uint32_t> ptr;
|
||||
} unicode;
|
||||
be<float> f32;
|
||||
struct {
|
||||
be<uint32_t> size;
|
||||
be<uint32_t> ptr;
|
||||
} binary;
|
||||
be<uint64_t> filetime;
|
||||
};
|
||||
};
|
||||
static_assert_size(X_USER_DATA, 16);
|
||||
|
||||
class DataByteStream : public ByteStream {
|
||||
public:
|
||||
DataByteStream(uint32_t ptr, uint8_t* data, size_t data_length,
|
||||
size_t offset = 0)
|
||||
: ByteStream(data, data_length, offset), ptr_(ptr) {}
|
||||
|
||||
uint32_t ptr() const { return static_cast<uint32_t>(ptr_ + offset()); }
|
||||
|
||||
private:
|
||||
uint32_t ptr_;
|
||||
};
|
||||
|
||||
class UserData {
|
||||
public:
|
||||
UserData() {};
|
||||
UserData(X_USER_DATA_TYPE type) { data_.type = type; }
|
||||
|
||||
virtual void Append(X_USER_DATA* data, DataByteStream* stream) {
|
||||
data->type = data_.type;
|
||||
}
|
||||
|
||||
virtual std::vector<uint8_t> Serialize() const {
|
||||
return std::vector<uint8_t>();
|
||||
}
|
||||
virtual void Deserialize(std::vector<uint8_t>) {}
|
||||
|
||||
private:
|
||||
X_USER_DATA data_ = {};
|
||||
};
|
||||
|
||||
class Int32UserData : public UserData {
|
||||
public:
|
||||
Int32UserData(int32_t value)
|
||||
: UserData(X_USER_DATA_TYPE::INT32), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->s32 = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
int32_t value_;
|
||||
};
|
||||
|
||||
class Uint32UserData : public UserData {
|
||||
public:
|
||||
Uint32UserData(uint32_t value)
|
||||
: UserData(X_USER_DATA_TYPE::INT32), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->u32 = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t value_;
|
||||
};
|
||||
|
||||
class Int64UserData : public UserData {
|
||||
public:
|
||||
Int64UserData(int64_t value)
|
||||
: UserData(X_USER_DATA_TYPE::INT64), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->s64 = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
int64_t value_;
|
||||
};
|
||||
|
||||
class FloatUserData : public UserData {
|
||||
public:
|
||||
FloatUserData(float value)
|
||||
: UserData(X_USER_DATA_TYPE::FLOAT), value_(value) {}
|
||||
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->f32 = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
float value_;
|
||||
};
|
||||
|
||||
class DoubleUserData : public UserData {
|
||||
public:
|
||||
DoubleUserData(double value)
|
||||
: UserData(X_USER_DATA_TYPE::DOUBLE), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->f64 = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
double value_;
|
||||
};
|
||||
|
||||
class UnicodeUserData : public UserData {
|
||||
public:
|
||||
UnicodeUserData(const std::u16string& value)
|
||||
: UserData(X_USER_DATA_TYPE::WSTRING), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
|
||||
if (value_.empty()) {
|
||||
data->unicode.size = 0;
|
||||
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();
|
||||
auto buffer =
|
||||
reinterpret_cast<uint16_t*>(&stream->data()[stream->offset()]);
|
||||
stream->Advance(size);
|
||||
copy_and_swap(buffer, (uint16_t*)value_.data(), count);
|
||||
}
|
||||
|
||||
private:
|
||||
std::u16string value_;
|
||||
};
|
||||
|
||||
class BinaryUserData : public UserData {
|
||||
public:
|
||||
BinaryUserData(const std::vector<uint8_t>& value)
|
||||
: UserData(X_USER_DATA_TYPE::BINARY), value_(value) {}
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
|
||||
if (value_.empty()) {
|
||||
data->binary.size = 0;
|
||||
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();
|
||||
stream->Write(value_.data(), size);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Serialize() const override {
|
||||
return std::vector<uint8_t>(value_.data(), value_.data() + value_.size());
|
||||
}
|
||||
|
||||
void Deserialize(std::vector<uint8_t> data) override { value_ = data; }
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> value_;
|
||||
};
|
||||
|
||||
class DateTimeUserData : public UserData {
|
||||
public:
|
||||
DateTimeUserData(int64_t value)
|
||||
: UserData(X_USER_DATA_TYPE::DATETIME), value_(value) {}
|
||||
|
||||
void Append(X_USER_DATA* data, DataByteStream* stream) override {
|
||||
UserData::Append(data, stream);
|
||||
data->filetime = value_;
|
||||
}
|
||||
|
||||
private:
|
||||
int64_t value_;
|
||||
};
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -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 {
|
||||
|
@ -40,48 +41,6 @@ enum class AchievementPlatform : uint32_t {
|
|||
kWebGames = 0x400000,
|
||||
};
|
||||
|
||||
enum class AchievementFlags : uint32_t {
|
||||
kTypeMask = 0x7,
|
||||
kShowUnachieved = 0x8,
|
||||
kAchievedOnline = 0x10000,
|
||||
kAchieved = 0x20000,
|
||||
kNotAchievable = 0x40000,
|
||||
kWasNotAchievable = 0x80000,
|
||||
kPlatformMask = 0x700000,
|
||||
kColorizable = 0x1000000, // avatar awards only?
|
||||
};
|
||||
|
||||
struct X_ACHIEVEMENT_UNLOCK_TIME {
|
||||
xe::be<uint32_t> high_part;
|
||||
xe::be<uint32_t> low_part;
|
||||
|
||||
X_ACHIEVEMENT_UNLOCK_TIME() {
|
||||
high_part = 0;
|
||||
low_part = 0;
|
||||
}
|
||||
|
||||
X_ACHIEVEMENT_UNLOCK_TIME(uint64_t filetime) {
|
||||
high_part = static_cast<uint32_t>(filetime >> 32);
|
||||
low_part = static_cast<uint32_t>(filetime);
|
||||
}
|
||||
|
||||
X_ACHIEVEMENT_UNLOCK_TIME(std::time_t time) {
|
||||
const auto file_time =
|
||||
chrono::WinSystemClock::to_file_time(chrono::WinSystemClock::from_sys(
|
||||
std::chrono::system_clock::from_time_t(time)));
|
||||
|
||||
high_part = static_cast<uint32_t>(file_time >> 32);
|
||||
low_part = static_cast<uint32_t>(file_time);
|
||||
}
|
||||
|
||||
chrono::WinSystemClock::time_point to_time_point() const {
|
||||
const uint64_t filetime =
|
||||
(static_cast<uint64_t>(high_part) << 32) | low_part;
|
||||
|
||||
return chrono::WinSystemClock::from_file_time(filetime);
|
||||
}
|
||||
};
|
||||
|
||||
struct X_ACHIEVEMENT_DETAILS {
|
||||
xe::be<uint32_t> id;
|
||||
xe::be<uint32_t> label_ptr;
|
||||
|
@ -89,13 +48,60 @@ struct X_ACHIEVEMENT_DETAILS {
|
|||
xe::be<uint32_t> unachieved_ptr;
|
||||
xe::be<uint32_t> image_id;
|
||||
xe::be<uint32_t> gamerscore;
|
||||
X_ACHIEVEMENT_UNLOCK_TIME unlock_time;
|
||||
X_FILETIME unlock_time;
|
||||
xe::be<uint32_t> flags;
|
||||
|
||||
static const size_t kStringBufferSize = 464;
|
||||
};
|
||||
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_FILETIME 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_FILETIME 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 +109,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,26 +146,23 @@ 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;
|
||||
X_ACHIEVEMENT_UNLOCK_TIME unlock_time;
|
||||
uint32_t achievement_id = 0;
|
||||
uint32_t image_id = 0;
|
||||
uint32_t gamerscore = 0;
|
||||
uint32_t flags = 0;
|
||||
X_FILETIME unlock_time;
|
||||
std::u16string achievement_name;
|
||||
std::u16string unlocked_description;
|
||||
std::u16string locked_description;
|
||||
|
||||
bool IsUnlocked() const {
|
||||
return (flags & static_cast<uint32_t>(AchievementFlags::kAchieved)) ||
|
||||
flags & static_cast<uint32_t>(AchievementFlags::kAchievedOnline);
|
||||
IsUnlockedOnline();
|
||||
}
|
||||
};
|
||||
|
||||
struct TitleAchievementsProfileInfo {
|
||||
uint32_t achievements_count;
|
||||
uint32_t unlocked_achievements_count;
|
||||
uint32_t gamerscore;
|
||||
bool IsUnlockedOnline() const {
|
||||
return (flags & static_cast<uint32_t>(AchievementFlags::kAchievedOnline));
|
||||
}
|
||||
};
|
||||
|
||||
class AchievementBackendInterface {
|
||||
|
@ -161,44 +176,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>>
|
||||
|
|
|
@ -24,11 +24,38 @@ namespace apps {
|
|||
* https://github.com/NicolasDe/AlienSwarm/blob/master/src/common/xbox/xboxstubs.h
|
||||
*/
|
||||
|
||||
struct X_XUSER_ACHIEVEMENT {
|
||||
struct XGI_XUSER_ACHIEVEMENT {
|
||||
xe::be<uint32_t> user_idx;
|
||||
xe::be<uint32_t> achievement_id;
|
||||
};
|
||||
|
||||
struct XGI_XUSER_GET_PROPERTY {
|
||||
xe::be<uint32_t> user_index;
|
||||
xe::be<uint32_t> unused;
|
||||
xe::be<uint64_t> xuid; // If xuid is 0 then user_index is used.
|
||||
xe::be<uint32_t>
|
||||
property_size_ptr; // Normally filled with sizeof(XUSER_PROPERTY), with
|
||||
// exception of binary and wstring type.
|
||||
xe::be<uint32_t> context_address;
|
||||
xe::be<uint32_t> property_address;
|
||||
};
|
||||
|
||||
struct XGI_XUSER_SET_CONTEXT {
|
||||
xe::be<uint32_t> user_index;
|
||||
xe::be<uint32_t> unused;
|
||||
xe::be<uint64_t> xuid;
|
||||
XUSER_CONTEXT context;
|
||||
};
|
||||
|
||||
struct XGI_XUSER_SET_PROPERTY {
|
||||
xe::be<uint32_t> user_index;
|
||||
xe::be<uint32_t> unused;
|
||||
xe::be<uint64_t> xuid;
|
||||
xe::be<uint32_t> property_id;
|
||||
xe::be<uint32_t> data_size;
|
||||
xe::be<uint32_t> data_address;
|
||||
};
|
||||
|
||||
struct XUSER_STATS_VIEW {
|
||||
xe::be<uint32_t> ViewId;
|
||||
xe::be<uint32_t> TotalViewRows;
|
||||
|
@ -63,63 +90,58 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
|||
auto buffer = memory_->TranslateVirtual(buffer_ptr);
|
||||
switch (message) {
|
||||
case 0x000B0006: {
|
||||
assert_true(!buffer_length || buffer_length == 24);
|
||||
// dword r3 user index
|
||||
// dword (unwritten?)
|
||||
// qword 0
|
||||
// dword r4 context enum
|
||||
// dword r5 value
|
||||
uint32_t user_index = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
uint32_t context_id = xe::load_and_swap<uint32_t>(buffer + 16);
|
||||
uint32_t context_value = xe::load_and_swap<uint32_t>(buffer + 20);
|
||||
XELOGD("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index,
|
||||
context_id, context_value);
|
||||
assert_true(!buffer_length ||
|
||||
buffer_length == sizeof(XGI_XUSER_SET_CONTEXT));
|
||||
const XGI_XUSER_SET_CONTEXT* xgi_context =
|
||||
reinterpret_cast<const XGI_XUSER_SET_CONTEXT*>(buffer);
|
||||
|
||||
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);
|
||||
XELOGD("XGIUserSetContext({:08X}, ID: {:08X}, Value: {:08X})",
|
||||
xgi_context->user_index.get(),
|
||||
xgi_context->context.context_id.get(),
|
||||
xgi_context->context.value.get());
|
||||
|
||||
UserProfile* user_profile =
|
||||
kernel_state_->xam_state()->GetUserProfile(user_index);
|
||||
if (user_profile) {
|
||||
user_profile->contexts_[context_id] = context_value;
|
||||
}
|
||||
UserProfile* user = nullptr;
|
||||
if (xgi_context->xuid != 0) {
|
||||
user = kernel_state_->xam_state()->GetUserProfile(xgi_context->xuid);
|
||||
} else {
|
||||
user =
|
||||
kernel_state_->xam_state()->GetUserProfile(xgi_context->user_index);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
kernel_state_->xam_state()->user_tracker()->UpdateContext(
|
||||
user->xuid(), xgi_context->context.context_id,
|
||||
xgi_context->context.value);
|
||||
}
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x000B0007: {
|
||||
uint32_t user_index = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
uint32_t property_id = xe::load_and_swap<uint32_t>(buffer + 16);
|
||||
uint32_t value_size = xe::load_and_swap<uint32_t>(buffer + 20);
|
||||
uint32_t value_ptr = xe::load_and_swap<uint32_t>(buffer + 24);
|
||||
XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", user_index,
|
||||
property_id, value_size, value_ptr);
|
||||
const XGI_XUSER_SET_PROPERTY* xgi_property =
|
||||
reinterpret_cast<const XGI_XUSER_SET_PROPERTY*>(buffer);
|
||||
|
||||
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);
|
||||
XELOGD("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})",
|
||||
xgi_property->user_index.get(), xgi_property->property_id.get(),
|
||||
xgi_property->data_size.get(), xgi_property->data_address.get());
|
||||
|
||||
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);
|
||||
UserProfile* user = nullptr;
|
||||
if (xgi_property->xuid != 0) {
|
||||
user = kernel_state_->xam_state()->GetUserProfile(xgi_property->xuid);
|
||||
} else {
|
||||
user = kernel_state_->xam_state()->GetUserProfile(
|
||||
xgi_property->user_index);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
Property property(
|
||||
xgi_property->property_id,
|
||||
UserSetting::get_valid_data_size(xgi_property->property_id,
|
||||
xgi_property->data_size),
|
||||
|
||||
memory_->TranslateVirtual<uint8_t*>(xgi_property->data_address));
|
||||
|
||||
kernel_state_->xam_state()->user_tracker()->AddProperty(user->xuid(),
|
||||
&property);
|
||||
}
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x000B0008: {
|
||||
|
@ -130,7 +152,7 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
|||
achievements_ptr);
|
||||
|
||||
auto* achievement =
|
||||
(X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr);
|
||||
memory_->TranslateVirtual<XGI_XUSER_ACHIEVEMENT*>(achievements_ptr);
|
||||
for (uint32_t i = 0; i < achievement_count; i++, achievement++) {
|
||||
kernel_state_->achievement_manager()->EarnAchievement(
|
||||
achievement->user_idx, kernel_state_->title_id(),
|
||||
|
@ -219,36 +241,68 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
|||
}
|
||||
case 0x000B003D: {
|
||||
// Used in 5451082A, 5553081E
|
||||
|
||||
// XUserGetCachedANID
|
||||
XELOGI("XUserGetANID({:08X}, {:08X}), implemented in netplay", buffer_ptr,
|
||||
buffer_length);
|
||||
return X_E_FAIL;
|
||||
}
|
||||
case 0x000B0041: {
|
||||
assert_true(!buffer_length || buffer_length == 32);
|
||||
// 00000000 2789fecc 00000000 00000000 200491e0 00000000 200491f0 20049340
|
||||
uint32_t user_index = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
uint32_t context_ptr = xe::load_and_swap<uint32_t>(buffer + 16);
|
||||
auto context =
|
||||
context_ptr ? memory_->TranslateVirtual(context_ptr) : nullptr;
|
||||
uint32_t context_id =
|
||||
context ? xe::load_and_swap<uint32_t>(context + 0) : 0;
|
||||
XELOGD("XGIUserGetContext({:08X}, {:08X}{:08X}))", user_index,
|
||||
context_ptr, context_id);
|
||||
uint32_t value = 0;
|
||||
if (context) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
xe::store_and_swap<uint32_t>(context + 4, value);
|
||||
assert_true(!buffer_length ||
|
||||
buffer_length == sizeof(XGI_XUSER_GET_PROPERTY));
|
||||
const XGI_XUSER_GET_PROPERTY* xgi_property =
|
||||
reinterpret_cast<const XGI_XUSER_GET_PROPERTY*>(buffer);
|
||||
|
||||
UserProfile* user = nullptr;
|
||||
if (xgi_property->xuid != 0) {
|
||||
user = kernel_state_->xam_state()->GetUserProfile(xgi_property->xuid);
|
||||
} else {
|
||||
user = kernel_state_->xam_state()->GetUserProfile(
|
||||
xgi_property->user_index);
|
||||
}
|
||||
return X_E_SUCCESS;
|
||||
|
||||
if (!user) {
|
||||
XELOGD(
|
||||
"XGIUserGetProperty - Invalid user provided: Index: {:08X} XUID: "
|
||||
"{:16X}",
|
||||
xgi_property->user_index.get(), xgi_property->xuid.get());
|
||||
return X_E_NOTFOUND;
|
||||
}
|
||||
|
||||
// Process context
|
||||
if (xgi_property->context_address) {
|
||||
XUSER_CONTEXT* context = memory_->TranslateVirtual<XUSER_CONTEXT*>(
|
||||
xgi_property->context_address);
|
||||
|
||||
XELOGD("XGIUserGetProperty - Context requested: {:08X} XUID: {:16X}",
|
||||
context->context_id.get(), user->xuid());
|
||||
|
||||
auto context_value =
|
||||
kernel_state_->xam_state()->user_tracker()->GetUserContext(
|
||||
user->xuid(), context->context_id);
|
||||
|
||||
if (!context_value) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
context->value = context_value.value();
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
|
||||
if (!xgi_property->property_size_ptr || !xgi_property->property_address) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
// Process property
|
||||
XUSER_PROPERTY* property = memory_->TranslateVirtual<XUSER_PROPERTY*>(
|
||||
xgi_property->property_address);
|
||||
|
||||
XELOGD("XGIUserGetProperty - Property requested: {:08X} XUID: {:16X}",
|
||||
property->property_id.get(), user->xuid());
|
||||
|
||||
return kernel_state_->xam_state()->user_tracker()->GetProperty(
|
||||
user->xuid(),
|
||||
memory_->TranslateVirtual<uint32_t*>(xgi_property->property_size_ptr),
|
||||
property);
|
||||
}
|
||||
case 0x000B0071: {
|
||||
XELOGD("XGIUnkB0071({:08X}, {:08X}), unimplemented", buffer_ptr,
|
||||
|
|
|
@ -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) {
|
||||
|
@ -440,12 +440,19 @@ uint8_t ProfileManager::GetUserIndexAssignedToProfile(
|
|||
}
|
||||
|
||||
std::filesystem::path ProfileManager::GetProfileContentPath(
|
||||
const uint64_t xuid, const uint32_t title_id) const {
|
||||
const uint64_t xuid, const uint32_t title_id,
|
||||
const XContentType content_type) const {
|
||||
std::filesystem::path profile_content_path =
|
||||
kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid);
|
||||
if (title_id != -1 && title_id != 0) {
|
||||
profile_content_path =
|
||||
profile_content_path / fmt::format("{:08X}", title_id);
|
||||
|
||||
if (content_type != XContentType::kInvalid) {
|
||||
profile_content_path =
|
||||
profile_content_path /
|
||||
fmt::format("{:08X}", static_cast<uint32_t>(content_type));
|
||||
}
|
||||
}
|
||||
return profile_content_path;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -34,33 +42,6 @@ constexpr uint32_t kDashboardID = 0xFFFE07D1;
|
|||
const static std::string kDashboardStringID =
|
||||
fmt::format("{:08X}", kDashboardID);
|
||||
|
||||
enum class XTileType {
|
||||
kAchievement,
|
||||
kGameIcon,
|
||||
kGamerTile,
|
||||
kGamerTileSmall,
|
||||
kLocalGamerTile,
|
||||
kLocalGamerTileSmall,
|
||||
kBkgnd,
|
||||
kAwardedGamerTile,
|
||||
kAwardedGamerTileSmall,
|
||||
kGamerTileByImageId,
|
||||
kPersonalGamerTile,
|
||||
kPersonalGamerTileSmall,
|
||||
kGamerTileByKey,
|
||||
kAvatarGamerTile,
|
||||
kAvatarGamerTileSmall,
|
||||
kAvatarFullBody
|
||||
};
|
||||
|
||||
// TODO: find filenames of other tile types that are stored in profile
|
||||
static const std::map<XTileType, std::string> kTileFileNames = {
|
||||
{XTileType::kPersonalGamerTile, "tile_64.png"},
|
||||
{XTileType::kPersonalGamerTileSmall, "tile_32.png"},
|
||||
{XTileType::kAvatarGamerTile, "avtr_64.png"},
|
||||
{XTileType::kAvatarGamerTileSmall, "avtr_32.png"},
|
||||
};
|
||||
|
||||
class ProfileManager {
|
||||
public:
|
||||
static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output,
|
||||
|
@ -75,7 +56,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();
|
||||
|
||||
|
@ -114,7 +95,8 @@ class ProfileManager {
|
|||
bool IsAnyProfileSignedIn() const { return !logged_profiles_.empty(); }
|
||||
|
||||
std::filesystem::path GetProfileContentPath(
|
||||
const uint64_t xuid, const uint32_t title_id = -1) const;
|
||||
const uint64_t xuid, const uint32_t title_id = -1,
|
||||
const XContentType content_type = XContentType::kInvalid) const;
|
||||
|
||||
static bool IsGamertagValid(const std::string gamertag);
|
||||
|
||||
|
@ -142,6 +124,7 @@ class ProfileManager {
|
|||
std::map<uint8_t, std::unique_ptr<UserProfile>> logged_profiles_;
|
||||
|
||||
KernelState* kernel_state_;
|
||||
UserTracker* user_tracker_;
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
|
|
|
@ -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/user_data.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
UserData::UserData() {}
|
||||
UserData::~UserData() {}
|
||||
|
||||
UserData::UserData(const UserData& user_data) {
|
||||
data_ = user_data.data_;
|
||||
extended_data_ = user_data.extended_data_;
|
||||
}
|
||||
|
||||
UserData::UserData(X_USER_DATA_TYPE data_type, UserDataTypes user_data) {
|
||||
data_.data = {};
|
||||
data_.type = data_type;
|
||||
|
||||
switch (data_type) {
|
||||
case X_USER_DATA_TYPE::BINARY:
|
||||
extended_data_ = std::get<std::vector<uint8_t>>(user_data);
|
||||
data_.data.binary.size = static_cast<uint16_t>(extended_data_.size());
|
||||
break;
|
||||
case X_USER_DATA_TYPE::WSTRING: {
|
||||
std::u16string str = std::get<std::u16string>(user_data);
|
||||
data_.data.unicode.size =
|
||||
static_cast<uint16_t>(string_util::size_in_bytes(str));
|
||||
|
||||
extended_data_.resize(data_.data.unicode.size);
|
||||
memcpy(extended_data_.data(), reinterpret_cast<uint8_t*>(str.data()),
|
||||
data_.data.unicode.size);
|
||||
break;
|
||||
}
|
||||
case X_USER_DATA_TYPE::INT32:
|
||||
data_.data.s32 = std::get<int32_t>(user_data);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::FLOAT:
|
||||
data_.data.f32 = std::get<float>(user_data);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::CONTEXT:
|
||||
data_.data.s32 = std::get<uint32_t>(user_data);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::DOUBLE:
|
||||
data_.data.f64 = std::get<double>(user_data);
|
||||
break;
|
||||
case X_USER_DATA_TYPE::DATETIME:
|
||||
case X_USER_DATA_TYPE::INT64:
|
||||
data_.data.s64 = std::get<int64_t>(user_data);
|
||||
break;
|
||||
default:
|
||||
assert_always();
|
||||
}
|
||||
}
|
||||
|
||||
UserData::UserData(const X_USER_DATA_TYPE data_type,
|
||||
const uint32_t data_max_size, const X_USER_DATA* user_data) {
|
||||
memcpy(&data_, user_data, sizeof(X_USER_DATA));
|
||||
data_.type = data_type;
|
||||
|
||||
if (requires_additional_data()) {
|
||||
data_.data.binary.size = std::min(
|
||||
static_cast<uint32_t>(data_.data.binary.size), kMaxUserDataSize);
|
||||
|
||||
if (!data_.data.binary.size) {
|
||||
data_.data.binary.size = data_max_size;
|
||||
}
|
||||
|
||||
extended_data_.resize(data_.data.binary.size);
|
||||
memcpy(
|
||||
extended_data_.data(),
|
||||
kernel_memory()->TranslateVirtual<uint8_t*>(user_data->data.binary.ptr),
|
||||
data_.data.binary.size);
|
||||
}
|
||||
}
|
||||
|
||||
UserData::UserData(X_USER_DATA_TYPE data_type, std::span<const uint8_t> data) {
|
||||
data_.data = {};
|
||||
data_.type = data_type;
|
||||
|
||||
if (!requires_additional_data()) {
|
||||
memcpy(&data_.data, data.data(),
|
||||
std::min(static_cast<size_t>(8), data.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
data_.data.binary.size = static_cast<uint32_t>(data.size());
|
||||
extended_data_.insert(extended_data_.begin(), data.begin(), data.end());
|
||||
}
|
||||
|
||||
UserData::UserData(const X_USER_DATA_TYPE data_type,
|
||||
const X_USER_DATA_UNION* user_data,
|
||||
std::span<const uint8_t> extended_data) {
|
||||
data_.type = data_type;
|
||||
data_.data = *user_data;
|
||||
|
||||
if (requires_additional_data()) {
|
||||
extended_data_.insert(extended_data_.begin(), extended_data.begin(),
|
||||
extended_data.end());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,180 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DATA_H_
|
||||
#define XENIA_KERNEL_XAM_USER_DATA_H_
|
||||
|
||||
#include <span>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
union AttributeKey {
|
||||
uint32_t value;
|
||||
struct {
|
||||
uint32_t id : 14;
|
||||
uint32_t unk : 2;
|
||||
uint32_t size : 12;
|
||||
uint32_t type : 4;
|
||||
};
|
||||
};
|
||||
|
||||
// ToDo: Check if setting CAN be set to unset. Property can and it will be
|
||||
// interpreted as a basic type property.
|
||||
enum class X_USER_DATA_TYPE : uint8_t {
|
||||
CONTEXT = 0,
|
||||
INT32 = 1,
|
||||
INT64 = 2,
|
||||
DOUBLE = 3,
|
||||
WSTRING = 4,
|
||||
FLOAT = 5,
|
||||
BINARY = 6,
|
||||
DATETIME = 7,
|
||||
UNSET = 0xFF,
|
||||
};
|
||||
|
||||
struct X_USER_DATA_UNION {
|
||||
union {
|
||||
be<int32_t> s32;
|
||||
be<int64_t> s64;
|
||||
be<uint32_t> u32;
|
||||
be<double> f64;
|
||||
struct {
|
||||
be<uint32_t> size;
|
||||
be<uint32_t> ptr;
|
||||
} unicode;
|
||||
be<float> f32;
|
||||
struct {
|
||||
be<uint32_t> size;
|
||||
be<uint32_t> ptr;
|
||||
} binary;
|
||||
be<uint64_t> filetime;
|
||||
};
|
||||
};
|
||||
static_assert_size(X_USER_DATA_UNION, 8);
|
||||
|
||||
struct alignas(8) X_USER_DATA {
|
||||
X_USER_DATA_TYPE type;
|
||||
X_USER_DATA_UNION data;
|
||||
};
|
||||
static_assert_size(X_USER_DATA, 16);
|
||||
|
||||
using UserDataTypes = std::variant<uint32_t, int32_t, float, int64_t, double,
|
||||
std::u16string, std::vector<uint8_t>>;
|
||||
|
||||
constexpr uint32_t kMaxUserDataSize = 0x03E8;
|
||||
|
||||
class UserData {
|
||||
public:
|
||||
X_USER_DATA_TYPE get_type() const { return data_.type; }
|
||||
|
||||
const X_USER_DATA* get_data() const { return &data_; }
|
||||
std::span<const uint8_t> get_extended_data() const {
|
||||
return {extended_data_.data(), extended_data_.size()};
|
||||
}
|
||||
|
||||
bool is_valid_type() const {
|
||||
return data_.type >= X_USER_DATA_TYPE::CONTEXT &&
|
||||
data_.type <= X_USER_DATA_TYPE::DATETIME;
|
||||
}
|
||||
|
||||
bool requires_additional_data() const {
|
||||
return data_.type == X_USER_DATA_TYPE::BINARY ||
|
||||
data_.type == X_USER_DATA_TYPE::WSTRING;
|
||||
}
|
||||
|
||||
static X_USER_DATA_TYPE get_type(uint32_t id) {
|
||||
return static_cast<X_USER_DATA_TYPE>(id >> 28);
|
||||
}
|
||||
static uint16_t get_max_size(uint32_t id) {
|
||||
return static_cast<uint16_t>(id >> 16) & kMaxUserDataSize;
|
||||
}
|
||||
|
||||
static bool requires_additional_data(uint32_t id) {
|
||||
const auto type = get_type(id);
|
||||
|
||||
return type == X_USER_DATA_TYPE::BINARY ||
|
||||
type == X_USER_DATA_TYPE::WSTRING;
|
||||
}
|
||||
|
||||
static uint32_t get_valid_data_size(uint32_t id, uint32_t provided_size) {
|
||||
if (!requires_additional_data(id)) {
|
||||
if (provided_size == sizeof(uint32_t) ||
|
||||
provided_size == sizeof(uint64_t)) {
|
||||
return provided_size;
|
||||
}
|
||||
|
||||
switch (get_type(id)) {
|
||||
case X_USER_DATA_TYPE::CONTEXT:
|
||||
case X_USER_DATA_TYPE::FLOAT:
|
||||
case X_USER_DATA_TYPE::INT32:
|
||||
return sizeof(uint32_t);
|
||||
|
||||
case X_USER_DATA_TYPE::DATETIME:
|
||||
case X_USER_DATA_TYPE::DOUBLE:
|
||||
case X_USER_DATA_TYPE::INT64:
|
||||
return sizeof(uint64_t);
|
||||
}
|
||||
|
||||
return sizeof(uint64_t);
|
||||
}
|
||||
|
||||
if (!provided_size) {
|
||||
return kMaxUserDataSize;
|
||||
}
|
||||
|
||||
return std::min(static_cast<uint32_t>(get_max_size(id)), provided_size);
|
||||
}
|
||||
|
||||
size_t get_data_size() const {
|
||||
return sizeof(X_USER_DATA) + extended_data_.size();
|
||||
}
|
||||
|
||||
static size_t get_data_size(uint32_t id, const X_USER_DATA* user_data) {
|
||||
if (requires_additional_data(id)) {
|
||||
return std::min(get_max_size(id),
|
||||
static_cast<uint16_t>(user_data->data.binary.size));
|
||||
}
|
||||
|
||||
return get_max_size(id);
|
||||
}
|
||||
|
||||
protected:
|
||||
~UserData();
|
||||
|
||||
UserData();
|
||||
UserData(const UserData& user_data);
|
||||
|
||||
// From host
|
||||
UserData(X_USER_DATA_TYPE data_type, UserDataTypes user_data);
|
||||
// From guest
|
||||
UserData(const X_USER_DATA_TYPE data_type, const uint32_t data_max_size,
|
||||
const X_USER_DATA* user_data);
|
||||
|
||||
// From guest - Property specific ctor. Property transfer raw data directly.
|
||||
UserData(X_USER_DATA_TYPE data_type, std::span<const uint8_t> data);
|
||||
|
||||
// For data from GPD
|
||||
UserData(const X_USER_DATA_TYPE data_type, const X_USER_DATA_UNION* user_data,
|
||||
std::span<const uint8_t> extended_data);
|
||||
|
||||
X_USER_DATA data_;
|
||||
std::vector<uint8_t> extended_data_ = {};
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -9,254 +9,150 @@
|
|||
|
||||
#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 {
|
||||
namespace xam {
|
||||
|
||||
UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info)
|
||||
: xuid_(xuid), account_info_(*account_info) {
|
||||
: xuid_(xuid), account_info_(*account_info), profile_images_() {
|
||||
// 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."
|
||||
LoadProfileGpds();
|
||||
|
||||
// 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>()));
|
||||
LoadProfileIcon(XTileType::kGamerTile);
|
||||
LoadProfileIcon(XTileType::kGamerTileSmall);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
GpdInfo* UserProfile::GetGpd(const uint32_t title_id) {
|
||||
return const_cast<GpdInfo*>(
|
||||
const_cast<const UserProfile*>(this)->GetGpd(title_id));
|
||||
}
|
||||
|
||||
UserSetting* UserProfile::GetSetting(uint32_t setting_id) {
|
||||
const auto& it = settings_.find(setting_id);
|
||||
if (it == settings_.end()) {
|
||||
const GpdInfo* UserProfile::GetGpd(const uint32_t title_id) const {
|
||||
if (title_id == kDashboardID) {
|
||||
return &dashboard_gpd_;
|
||||
}
|
||||
|
||||
if (!games_gpd_.count(title_id)) {
|
||||
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;
|
||||
return &games_gpd_.at(title_id);
|
||||
}
|
||||
|
||||
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;
|
||||
void UserProfile::LoadProfileGpds() {
|
||||
// First load dashboard GPD because it stores all opened games
|
||||
dashboard_gpd_ = LoadGpd(kDashboardID);
|
||||
if (!dashboard_gpd_.IsValid()) {
|
||||
dashboard_gpd_ = GpdInfoProfile();
|
||||
}
|
||||
|
||||
properties_.push_back(*property);
|
||||
return true;
|
||||
}
|
||||
const auto gpds_to_load = dashboard_gpd_.GetTitlesInfo();
|
||||
|
||||
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;
|
||||
void UserProfile::LoadProfileIcon(XTileType tile_type) {
|
||||
if (!kTileFileNames.count(tile_type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& entry : title_achievements->second) {
|
||||
if (entry.achievement_id == id) {
|
||||
return &entry;
|
||||
}
|
||||
const std::string path =
|
||||
fmt::format("{:016X}:\\{}", xuid_, kTileFileNames.at(tile_type));
|
||||
|
||||
vfs::File* file = nullptr;
|
||||
vfs::FileAction action;
|
||||
|
||||
const X_STATUS result = kernel_state()->file_system()->OpenFile(
|
||||
nullptr, path, vfs::FileDisposition::kOpen, vfs::FileAccess::kGenericRead,
|
||||
false, true, &file, &action);
|
||||
|
||||
if (result != X_STATUS_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
std::vector<uint8_t> data(file->entry()->size());
|
||||
size_t written_bytes = 0;
|
||||
file->ReadSync(data.data(), file->entry()->size(), 0, &written_bytes);
|
||||
file->Destroy();
|
||||
|
||||
profile_images_.insert({tile_type, 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;
|
||||
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 {};
|
||||
}
|
||||
|
||||
return &title_achievements->second;
|
||||
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 {};
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
||||
file->Destroy();
|
||||
return data;
|
||||
}
|
||||
|
||||
bool UserProfile::WriteGpd(const uint32_t title_id) {
|
||||
const GpdInfo* gpd = GetGpd(title_id);
|
||||
if (!gpd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data = gpd->Serialize();
|
||||
|
||||
vfs::File* file = nullptr;
|
||||
vfs::FileAction action;
|
||||
|
||||
const std::string mounted_path =
|
||||
fmt::format("{:016X}:\\{:08X}.gpd", xuid_, title_id);
|
||||
|
||||
const X_STATUS result = kernel_state()->file_system()->OpenFile(
|
||||
nullptr, mounted_path, vfs::FileDisposition::kOverwriteIf,
|
||||
vfs::FileAccess::kGenericWrite, false, true, &file, &action);
|
||||
|
||||
if (result != X_STATUS_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written_bytes = 0;
|
||||
file->WriteSync(data.data(), data.size(), 0, &written_bytes);
|
||||
file->Destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
|
|
|
@ -16,63 +16,24 @@
|
|||
#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/user_property.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,86 +46,33 @@ 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;
|
||||
enum class XTileType {
|
||||
kAchievement,
|
||||
kGameIcon,
|
||||
kGamerTile,
|
||||
kGamerTileSmall,
|
||||
kLocalGamerTile,
|
||||
kLocalGamerTileSmall,
|
||||
kBkgnd,
|
||||
kAwardedGamerTile,
|
||||
kAwardedGamerTileSmall,
|
||||
kGamerTileByImageId,
|
||||
kPersonalGamerTile,
|
||||
kPersonalGamerTileSmall,
|
||||
kGamerTileByKey,
|
||||
kAvatarGamerTile,
|
||||
kAvatarGamerTileSmall,
|
||||
kAvatarFullBody
|
||||
};
|
||||
|
||||
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;
|
||||
// TODO: find filenames of other tile types that are stored in profile
|
||||
static const std::map<XTileType, std::string> kTileFileNames = {
|
||||
{XTileType::kGamerTile, "tile_64.png"},
|
||||
{XTileType::kGamerTileSmall, "tile_32.png"},
|
||||
{XTileType::kPersonalGamerTile, "tile_64.png"},
|
||||
{XTileType::kPersonalGamerTileSmall, "tile_32.png"},
|
||||
{XTileType::kAvatarGamerTile, "avtr_64.png"},
|
||||
{XTileType::kAvatarGamerTileSmall, "avtr_32.png"},
|
||||
};
|
||||
|
||||
class UserProfile {
|
||||
|
@ -180,39 +88,50 @@ class UserProfile {
|
|||
uint32_t GetSubscriptionTier() const {
|
||||
return account_info_.GetSubscriptionTier();
|
||||
}
|
||||
|
||||
std::span<const uint8_t> GetProfileIcon(XTileType icon_type) {
|
||||
// Overwrite same types?
|
||||
if (icon_type == XTileType::kPersonalGamerTile) {
|
||||
icon_type = XTileType::kGamerTile;
|
||||
}
|
||||
|
||||
if (icon_type == XTileType::kPersonalGamerTileSmall) {
|
||||
icon_type = XTileType::kGamerTileSmall;
|
||||
}
|
||||
|
||||
if (profile_images_.find(icon_type) == profile_images_.cend()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {profile_images_[icon_type].data(),
|
||||
profile_images_[icon_type].size()};
|
||||
}
|
||||
|
||||
void GetPasscode(uint16_t* passcode) const {
|
||||
std::memcpy(passcode, account_info_.passcode,
|
||||
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::vector<Property> properties_; // Includes contexts!
|
||||
|
||||
std::vector<Property> properties_;
|
||||
std::map<XTileType, std::vector<uint8_t>> profile_images_;
|
||||
|
||||
void LoadSetting(UserSetting*);
|
||||
void SaveSetting(UserSetting*);
|
||||
GpdInfo* GetGpd(const uint32_t title_id);
|
||||
const GpdInfo* GetGpd(const uint32_t title_id) const;
|
||||
|
||||
void LoadProfileGpds();
|
||||
void LoadProfileIcon(XTileType tile_type);
|
||||
std::vector<uint8_t> LoadGpd(const uint32_t title_id);
|
||||
bool WriteGpd(const uint32_t title_id);
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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/user_property.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
Property::Property() : UserData() {};
|
||||
|
||||
Property::Property(const Property& property)
|
||||
: UserData(property), property_id_(property.property_id_) {}
|
||||
|
||||
Property::Property(uint32_t property_id, UserDataTypes property_data)
|
||||
: UserData(get_type(property_id), property_data) {
|
||||
property_id_ = static_cast<AttributeKey>(property_id);
|
||||
}
|
||||
|
||||
Property::Property(uint32_t property_id, uint32_t value_size,
|
||||
uint8_t* value_ptr)
|
||||
: UserData(get_type(property_id),
|
||||
std::span<const uint8_t>(value_ptr, value_size)) {
|
||||
property_id_.value = property_id;
|
||||
}
|
||||
|
||||
Property::Property(const uint8_t* serialized_data, size_t data_size)
|
||||
: UserData(X_USER_DATA_TYPE::CONTEXT, std::span<const uint8_t>({})) {}
|
||||
|
||||
Property::~Property() {};
|
||||
|
||||
std::vector<uint8_t> Property::Serialize() const {
|
||||
std::vector<uint8_t> serialized_property(sizeof(XUSER_PROPERTY) +
|
||||
extended_data_.size());
|
||||
|
||||
memcpy(serialized_property.data(), &property_id_, sizeof(AttributeKey));
|
||||
memcpy(serialized_property.data() + sizeof(AttributeKey), &data_,
|
||||
sizeof(X_USER_DATA));
|
||||
|
||||
if (requires_additional_data()) {
|
||||
memcpy(
|
||||
serialized_property.data() + sizeof(AttributeKey) + sizeof(X_USER_DATA),
|
||||
extended_data_.data(), extended_data_.size());
|
||||
}
|
||||
return serialized_property;
|
||||
}
|
||||
|
||||
void Property::WriteToGuest(XUSER_PROPERTY* property) const {
|
||||
if (!property) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requires_additional_data()) {
|
||||
property->data.type = data_.type;
|
||||
property->data.data.binary.size =
|
||||
static_cast<uint32_t>(extended_data_.size());
|
||||
|
||||
memcpy(kernel_memory()->TranslateVirtual(property->data.data.binary.ptr),
|
||||
extended_data_.data(), extended_data_.size());
|
||||
} else {
|
||||
memcpy(&property->data, &data_, sizeof(X_USER_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -7,29 +7,36 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_UTIL_PROPERTY_H_
|
||||
#define XENIA_KERNEL_UTIL_PROPERTY_H_
|
||||
#ifndef XENIA_KERNEL_XAM_USER_PROPERTY_H_
|
||||
#define XENIA_KERNEL_XAM_USER_PROPERTY_H_
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "xenia/base/byte_stream.h"
|
||||
#include "xenia/kernel/util/xuserdata.h"
|
||||
#include "xenia/kernel/xam/user_data.h"
|
||||
#include "xenia/memory.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
// This structure is here because context is a one type of property.
|
||||
struct XUSER_CONTEXT {
|
||||
xe::be<uint32_t> context_id;
|
||||
xe::be<uint32_t> value;
|
||||
};
|
||||
|
||||
struct XUSER_PROPERTY {
|
||||
xe::be<uint32_t> property_id;
|
||||
X_USER_DATA data;
|
||||
};
|
||||
|
||||
using userDataVariant = std::variant<uint32_t, uint64_t, float, double,
|
||||
std::u16string, std::vector<uint8_t> >;
|
||||
|
||||
class Property {
|
||||
class Property : public UserData {
|
||||
public:
|
||||
Property();
|
||||
Property(const Property& property);
|
||||
|
||||
Property(uint32_t property_id, UserDataTypes setting_data);
|
||||
// Ctor used while guest is creating property.
|
||||
Property(uint32_t property_id, uint32_t value_size, uint8_t* value_ptr);
|
||||
// Ctor used for deserialization
|
||||
|
@ -39,32 +46,15 @@ class Property {
|
|||
const AttributeKey GetPropertyId() const { return property_id_; }
|
||||
|
||||
bool IsValid() const { return property_id_.value != 0; }
|
||||
bool IsContext() const { return data_.type == X_USER_DATA_TYPE::CONTEXT; }
|
||||
void WriteToGuest(XUSER_PROPERTY* property) const;
|
||||
std::vector<uint8_t> Serialize() const;
|
||||
|
||||
// Writer back to guest structure
|
||||
void Write(Memory* memory, XUSER_PROPERTY* property) const;
|
||||
uint32_t GetSize() const { return value_size_; }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Returns variant for specific value in LE (host) notation.
|
||||
userDataVariant GetValue() const;
|
||||
|
||||
private:
|
||||
AttributeKey property_id_ = {};
|
||||
X_USER_DATA_TYPE data_type_ = X_USER_DATA_TYPE::UNSET;
|
||||
|
||||
uint32_t value_size_ = 0;
|
||||
std::vector<uint8_t> value_;
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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 "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(UserSetting& setting) : UserData(setting) {
|
||||
setting_id_ = setting.setting_id_;
|
||||
setting_source_ = setting.setting_source_;
|
||||
}
|
||||
|
||||
UserSetting::UserSetting(UserSettingId setting_id, UserDataTypes setting_data)
|
||||
: UserData(get_type(static_cast<uint32_t>(setting_id)), setting_data),
|
||||
setting_id_(setting_id),
|
||||
setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {}
|
||||
|
||||
UserSetting::UserSetting(const X_USER_PROFILE_SETTING* profile_setting)
|
||||
: UserData(UserSetting::get_type(profile_setting->setting_id),
|
||||
UserSetting::get_max_size(profile_setting->setting_id),
|
||||
&profile_setting->data),
|
||||
setting_id_(
|
||||
static_cast<UserSettingId>(profile_setting->setting_id.get())),
|
||||
setting_source_(X_USER_PROFILE_SETTING_SOURCE::DEFAULT) {}
|
||||
|
||||
UserSetting::UserSetting(const X_XDBF_GPD_SETTING_HEADER* profile_setting,
|
||||
std::span<const uint8_t> extended_data)
|
||||
: UserData(profile_setting->setting_type, &profile_setting->base_data,
|
||||
extended_data),
|
||||
setting_id_(
|
||||
static_cast<UserSettingId>(profile_setting->setting_id.get())),
|
||||
setting_source_(X_USER_PROFILE_SETTING_SOURCE::TITLE) {}
|
||||
|
||||
std::optional<UserSetting> UserSetting::GetDefaultSetting(
|
||||
const UserProfile* user, uint32_t setting_id) {
|
||||
const auto type = UserData::get_type(setting_id);
|
||||
|
||||
switch (type) {
|
||||
case X_USER_DATA_TYPE::CONTEXT:
|
||||
case X_USER_DATA_TYPE::INT32:
|
||||
case X_USER_DATA_TYPE::UNSET:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), 0);
|
||||
case X_USER_DATA_TYPE::INT64:
|
||||
case X_USER_DATA_TYPE::DATETIME:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), static_cast<int64_t>(0));
|
||||
case X_USER_DATA_TYPE::DOUBLE:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), 0.0);
|
||||
case X_USER_DATA_TYPE::WSTRING:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), std::u16string());
|
||||
case X_USER_DATA_TYPE::FLOAT:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), 0.0f);
|
||||
case X_USER_DATA_TYPE::BINARY:
|
||||
return std::make_optional<UserSetting>(
|
||||
static_cast<UserSettingId>(setting_id), std::vector<uint8_t>());
|
||||
default:
|
||||
assert_always();
|
||||
}
|
||||
|
||||
XELOGE("{}: Unknown X_USER_DATA_TYPE: {}", __func__,
|
||||
static_cast<uint8_t>(type));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void UserSetting::WriteToGuest(X_USER_PROFILE_SETTING* setting_ptr,
|
||||
uint32_t& extended_data_address) {
|
||||
if (!setting_ptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(&setting_ptr->data.data, &data_.data, sizeof(X_USER_DATA_UNION));
|
||||
setting_ptr->data.type = data_.type;
|
||||
|
||||
if (requires_additional_data()) {
|
||||
const auto extended_data = get_extended_data();
|
||||
|
||||
setting_ptr->data.data.binary.size =
|
||||
static_cast<uint32_t>(extended_data_.size());
|
||||
setting_ptr->data.data.binary.ptr = extended_data_address;
|
||||
|
||||
memcpy(kernel_memory()->TranslateVirtual(extended_data_address),
|
||||
extended_data_.data(), extended_data_.size());
|
||||
|
||||
extended_data_address += static_cast<uint32_t>(extended_data_.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> UserSetting::Serialize() 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 = data_.type;
|
||||
|
||||
memcpy(&header.base_data, &data_.data, sizeof(X_USER_DATA_UNION));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,455 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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/xam/profile_manager.h"
|
||||
#include "xenia/kernel/xam/user_data.h"
|
||||
#include "xenia/kernel/xam/user_profile.h"
|
||||
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
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, kMaxUserDataSize, 0x43), // 0x43E80043,
|
||||
|
||||
XPROFILE_CRUX_BIO = SettingKey(X_USER_DATA_TYPE::WSTRING, kMaxUserDataSize,
|
||||
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, kMaxUserDataSize,
|
||||
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, kMaxUserDataSize, 0x44), // 0x63E80044,
|
||||
XPROFILE_GAMERCARD_AVATAR_INFO_2 = SettingKey(
|
||||
X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x45), // 0x63E80045,
|
||||
XPROFILE_GAMERCARD_PARTY_INFO =
|
||||
SettingKey(X_USER_DATA_TYPE::BINARY, 0x100, 0x46), // 0x61000046,
|
||||
|
||||
XPROFILE_TITLE_SPECIFIC1 = SettingKey(
|
||||
X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x3FFF), // 0x63E83FFF,
|
||||
XPROFILE_TITLE_SPECIFIC2 = SettingKey(
|
||||
X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 0x3FFE), // 0x63E83FFE,
|
||||
XPROFILE_TITLE_SPECIFIC3 = SettingKey(
|
||||
X_USER_DATA_TYPE::BINARY, kMaxUserDataSize, 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
|
||||
};
|
||||
|
||||
class UserSetting : public UserData {
|
||||
public:
|
||||
UserSetting(UserSetting& setting);
|
||||
// Ctor for writing from host
|
||||
UserSetting(UserSettingId setting_id, UserDataTypes 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);
|
||||
|
||||
static std::optional<UserSetting> GetDefaultSetting(const UserProfile* user,
|
||||
uint32_t setting_id);
|
||||
void WriteToGuest(X_USER_PROFILE_SETTING* setting_ptr,
|
||||
uint32_t& extended_data_address);
|
||||
std::vector<uint8_t> Serialize() const;
|
||||
|
||||
uint32_t get_setting_id() const { return static_cast<uint32_t>(setting_id_); }
|
||||
X_USER_PROFILE_SETTING_SOURCE get_setting_source() const {
|
||||
return setting_source_;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private:
|
||||
UserSettingId setting_id_;
|
||||
X_USER_PROFILE_SETTING_SOURCE setting_source_;
|
||||
|
||||
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_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,781 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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/emulator.h"
|
||||
#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_data.h"
|
||||
#include "xenia/kernel/xam/user_property.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!", __func__);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!spa_data_) {
|
||||
XELOGW("{}: Missing title SPA.", __func__);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto spa_achievement = spa_data_->GetAchievement(achievement_id);
|
||||
if (!spa_achievement) {
|
||||
XELOGW("{}: Missing achievement data in SPA.", __func__);
|
||||
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) {
|
||||
XELOGW(
|
||||
"{}: Missing achievement data in title GPD. (User: {} Title: {:08X})",
|
||||
__func__, user->name(), spa_data_->title_id());
|
||||
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::FlushUserData(const uint64_t xuid) {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
user->WriteGpd(kDashboardID);
|
||||
|
||||
if (spa_data_) {
|
||||
user->WriteGpd(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 = user->dashboard_gpd_.GetTitleName(title_id);
|
||||
info.icon = game_gpd->second.GetImage(kXdbfIdTitle);
|
||||
|
||||
if (title_data->last_played.is_valid()) {
|
||||
info.last_played = chrono::WinSystemClock::to_local(
|
||||
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.flags = title_data->flags;
|
||||
info.all_avatar_awards = title_data->all_avatar_awards;
|
||||
info.male_avatar_awards = title_data->male_avatar_awards;
|
||||
info.female_avatar_awards = title_data->female_avatar_awards;
|
||||
info.online_unlocked_achievements = title_data->online_achievement_count;
|
||||
info.title_name = user->dashboard_gpd_.GetTitleName(title_data->title_id);
|
||||
|
||||
if (title_data->last_played.is_valid()) {
|
||||
info.last_played = chrono::WinSystemClock::to_local(
|
||||
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::UpdateMissingAchievemntsIcons() {
|
||||
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;
|
||||
}
|
||||
|
||||
for (const auto& id : game_gpd->second.GetAchievementsIds()) {
|
||||
const auto entry = game_gpd->second.GetAchievementEntry(id);
|
||||
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry->is_achievement_unlocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!game_gpd->second.GetImage(entry->image_id).empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
game_gpd->second.AddImage(entry->image_id,
|
||||
spa_data_->GetIcon(entry->image_id));
|
||||
}
|
||||
user->WriteGpd(spa_data_->title_id());
|
||||
}
|
||||
}
|
||||
|
||||
void UserTracker::UpdateSpaInfo(SpaInfo* spa_info) {
|
||||
spa_data_ = spa_info;
|
||||
|
||||
if (!spa_data_) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProfileGpd();
|
||||
UpdateTitleGpdFile();
|
||||
UpdateMissingAchievemntsIcons();
|
||||
}
|
||||
|
||||
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) {
|
||||
X_XDBF_GPD_TITLE_PLAYED 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) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetIcon(xuid, title_id, XTileType::kAchievement, entry->image_id);
|
||||
}
|
||||
|
||||
void UserTracker::AddProperty(const uint64_t xuid, const Property* property) {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto property_id = property->GetPropertyId();
|
||||
|
||||
if (property->IsContext()) {
|
||||
const auto context_data = spa_data_->GetContext(property_id.value);
|
||||
if (!context_data) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const auto property_data = spa_data_->GetProperty(property_id.value);
|
||||
if (!property_data) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto entry = std::find_if(user->properties_.begin(), user->properties_.end(),
|
||||
[property_id](const Property& property_data) {
|
||||
return property_data.GetPropertyId().value ==
|
||||
property_id.value;
|
||||
});
|
||||
|
||||
if (entry != user->properties_.end()) {
|
||||
*entry = *property;
|
||||
return;
|
||||
}
|
||||
|
||||
user->properties_.push_back(*property);
|
||||
}
|
||||
|
||||
X_STATUS UserTracker::GetProperty(const uint64_t xuid, uint32_t* property_size,
|
||||
XUSER_PROPERTY* property) {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return X_E_NOTFOUND;
|
||||
}
|
||||
|
||||
*property_size = 0;
|
||||
const auto property_id = property->property_id;
|
||||
|
||||
const auto entry =
|
||||
std::find_if(user->properties_.cbegin(), user->properties_.cend(),
|
||||
[property_id](const Property& property_data) {
|
||||
return property_data.GetPropertyId().value == property_id;
|
||||
});
|
||||
|
||||
if (entry == user->properties_.cend()) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
if (entry->requires_additional_data()) {
|
||||
if (!property->data.data.binary.ptr) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
}
|
||||
|
||||
*property_size = static_cast<uint32_t>(entry->get_data_size());
|
||||
entry->WriteToGuest(property);
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
|
||||
const Property* UserTracker::GetProperty(const uint64_t xuid,
|
||||
const uint32_t id) const {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto entry =
|
||||
std::find_if(user->properties_.cbegin(), user->properties_.cend(),
|
||||
[id](const Property& property_data) {
|
||||
return property_data.GetPropertyId().value == id;
|
||||
});
|
||||
|
||||
if (entry == user->properties_.cend()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &(*entry);
|
||||
}
|
||||
|
||||
std::optional<UserSetting> UserTracker::GetGpdSetting(
|
||||
UserProfile* user, uint32_t title_id, 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(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::GetSetting(UserProfile* user,
|
||||
uint32_t title_id,
|
||||
uint32_t setting_id) const {
|
||||
auto gpd_setting = GetGpdSetting(user, title_id, setting_id);
|
||||
if (gpd_setting) {
|
||||
return gpd_setting.value();
|
||||
}
|
||||
|
||||
return UserSetting::GetDefaultSetting(user, setting_id);
|
||||
}
|
||||
|
||||
bool UserTracker::GetUserSetting(uint64_t xuid, uint32_t title_id,
|
||||
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 setting = GetSetting(user, title_id, setting_id);
|
||||
if (!setting) {
|
||||
return false;
|
||||
}
|
||||
setting_ptr->setting_id = setting_id;
|
||||
setting_ptr->source = setting->get_setting_source();
|
||||
|
||||
setting->WriteToGuest(setting_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;
|
||||
}
|
||||
|
||||
if (value > context_data->max_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto entry =
|
||||
std::find_if(user->properties_.begin(), user->properties_.end(),
|
||||
[id](const Property& property_data) {
|
||||
return property_data.IsContext() &&
|
||||
property_data.GetPropertyId().value == id;
|
||||
});
|
||||
|
||||
if (entry != user->properties_.cend()) {
|
||||
*entry = Property(id, value);
|
||||
return;
|
||||
}
|
||||
|
||||
user->properties_.push_back(Property(id, 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;
|
||||
}
|
||||
|
||||
const auto entry = std::find_if(
|
||||
user->properties_.cbegin(), user->properties_.cend(),
|
||||
[id](const Property& property_data) {
|
||||
return property_data.get_type() == X_USER_DATA_TYPE::CONTEXT &&
|
||||
property_data.GetPropertyId().value == id;
|
||||
});
|
||||
|
||||
if (entry == user->properties_.cend()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return entry->get_data()->data.u32;
|
||||
}
|
||||
|
||||
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 = user->GetGpd(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 = user->GetGpd(title_id);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
info->UpsertSetting(setting);
|
||||
FlushUserData(xuid);
|
||||
}
|
||||
|
||||
std::span<const uint8_t> UserTracker::GetIcon(uint64_t xuid, uint32_t title_id,
|
||||
XTileType tile_type,
|
||||
uint64_t tile_id) const {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!title_id) {
|
||||
if (kernel_state()->emulator()->is_title_open()) {
|
||||
title_id = kernel_state()->title_id();
|
||||
}
|
||||
}
|
||||
|
||||
switch (tile_type) {
|
||||
case XTileType::kAchievement: {
|
||||
if (title_id == kernel_state()->title_id()) {
|
||||
return spa_data_->GetIcon(tile_id);
|
||||
} else {
|
||||
const auto gpd = user->GetGpd(title_id);
|
||||
if (!gpd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return gpd->GetImage(tile_id);
|
||||
}
|
||||
}
|
||||
case XTileType::kGameIcon: {
|
||||
const auto gpd = user->GetGpd(title_id);
|
||||
if (!gpd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return gpd->GetImage(tile_id);
|
||||
}
|
||||
case XTileType::kGamerTile:
|
||||
case XTileType::kGamerTileSmall:
|
||||
case XTileType::kPersonalGamerTile:
|
||||
case XTileType::kPersonalGamerTileSmall:
|
||||
return user->GetProfileIcon(tile_type);
|
||||
|
||||
default:
|
||||
XELOGW("{}: Unsupported tile_type: {:08X} for title: {:08X} Id: {:16X}",
|
||||
__func__, static_cast<uint32_t>(tile_type), title_id, tile_id);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void UserTracker::RefreshTitleSummary(uint64_t xuid, uint32_t title_id) {
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(xuid);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto profile_gpd = user->GetGpd(kDashboardID);
|
||||
if (!profile_gpd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title_gpd =
|
||||
reinterpret_cast<GpdInfoTitle*>(user->GetGpd(title_id));
|
||||
if (!title_gpd) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto title_data = user->dashboard_gpd_.GetTitleInfo(title_id);
|
||||
if (!title_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
title_data->achievements_count = title_gpd->GetAchievementCount();
|
||||
title_data->achievements_unlocked = title_gpd->GetUnlockedAchievementCount();
|
||||
title_data->gamerscore_total = title_gpd->GetTotalGamerscore();
|
||||
title_data->gamerscore_earned = title_gpd->GetGamerscore();
|
||||
|
||||
user->WriteGpd(kDashboardID);
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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::u16string title_name;
|
||||
uint32_t id;
|
||||
uint32_t unlocked_achievements_count;
|
||||
uint32_t achievements_count;
|
||||
uint32_t title_earned_gamerscore;
|
||||
uint32_t gamerscore_amount;
|
||||
uint32_t flags;
|
||||
uint16_t online_unlocked_achievements;
|
||||
X_XDBF_AVATARAWARDS_COUNTER all_avatar_awards;
|
||||
X_XDBF_AVATARAWARDS_COUNTER male_avatar_awards;
|
||||
X_XDBF_AVATARAWARDS_COUNTER female_avatar_awards;
|
||||
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);
|
||||
void RefreshTitleSummary(uint64_t xuid, uint32_t title_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);
|
||||
X_STATUS GetProperty(const uint64_t xuid, uint32_t* property_size,
|
||||
XUSER_PROPERTY* property);
|
||||
const Property* GetProperty(const uint64_t xuid, const uint32_t id) const;
|
||||
|
||||
// Settings
|
||||
void UpsertSetting(uint64_t xuid, uint32_t title_id,
|
||||
const UserSetting* setting);
|
||||
|
||||
bool GetUserSetting(uint64_t xuid, uint32_t title_id, 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;
|
||||
|
||||
// Images
|
||||
std::span<const uint8_t> GetIcon(uint64_t xuid, uint32_t title_id,
|
||||
XTileType tile_type, uint64_t tile_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> GetSetting(UserProfile* user, uint32_t title_id,
|
||||
uint32_t setting_id) const;
|
||||
std::optional<UserSetting> GetGpdSetting(UserProfile* user, uint32_t title_id,
|
||||
uint32_t setting_id) const;
|
||||
|
||||
void AddTitleToPlayedList(uint64_t xuid);
|
||||
void UpdateTitleGpdFile();
|
||||
void UpdateProfileGpd();
|
||||
void UpdateMissingAchievemntsIcons();
|
||||
|
||||
void FlushUserData(const uint64_t xuid);
|
||||
|
||||
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"
|
||||
|
@ -499,36 +500,6 @@ class GamertagModifyDialog final : public ui::ImGuiDialog {
|
|||
ProfileManager* profile_manager_;
|
||||
};
|
||||
|
||||
struct AchievementInfo {
|
||||
uint32_t id;
|
||||
std::u16string name;
|
||||
std::u16string desc;
|
||||
std::u16string unachieved;
|
||||
uint32_t gamerscore;
|
||||
uint32_t image_id;
|
||||
uint32_t flags;
|
||||
std::chrono::local_time<std::chrono::system_clock::duration> unlock_time;
|
||||
|
||||
bool IsUnlocked() const {
|
||||
return (flags & static_cast<uint32_t>(AchievementFlags::kAchieved)) ||
|
||||
flags & static_cast<uint32_t>(AchievementFlags::kAchievedOnline);
|
||||
}
|
||||
|
||||
// Unlocked online means that unlock time is confirmed and valid!
|
||||
bool IsUnlockedOnline() const {
|
||||
return (flags & static_cast<uint32_t>(AchievementFlags::kAchievedOnline));
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
|
@ -546,98 +517,97 @@ class GameAchievementsDialog final : public XamDialog {
|
|||
bool LoadAchievementsData() {
|
||||
xe::ui::IconsData data;
|
||||
|
||||
const auto title_achievements =
|
||||
achievements_info_ =
|
||||
kernel_state()
|
||||
->xam_state()
|
||||
->achievement_manager()
|
||||
->GetTitleAchievements(profile_->xuid(), title_info_.id);
|
||||
|
||||
const auto title_gpd = kernel_state()->title_xdbf();
|
||||
|
||||
if (!title_achievements) {
|
||||
if (achievements_info_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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());
|
||||
for (const Achievement& entry : achievements_info_) {
|
||||
const auto icon =
|
||||
kernel_state()
|
||||
->xam_state()
|
||||
->achievement_manager()
|
||||
->GetAchievementIcon(profile_->xuid(), title_info_.id,
|
||||
entry.achievement_id);
|
||||
|
||||
info.flags = entry.flags;
|
||||
info.gamerscore = entry.gamerscore;
|
||||
info.image_id = entry.image_id;
|
||||
info.unlock_time = {};
|
||||
|
||||
if (entry.IsUnlocked()) {
|
||||
info.unlock_time =
|
||||
chrono::WinSystemClock::to_local(entry.unlock_time.to_time_point());
|
||||
}
|
||||
|
||||
achievements_info_.insert({info.id, info});
|
||||
|
||||
const auto& icon_entry =
|
||||
title_gpd.GetEntry(util::XdbfSection::kImage, info.image_id);
|
||||
|
||||
data.insert({info.image_id,
|
||||
std::make_pair(icon_entry.buffer,
|
||||
static_cast<uint32_t>(icon_entry.size))});
|
||||
data.insert({entry.image_id, icon});
|
||||
}
|
||||
|
||||
achievements_icons_ = imgui_drawer()->LoadIcons(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GetAchievementTitle(const AchievementInfo& achievement_entry) {
|
||||
std::string GetAchievementTitle(const Achievement& achievement_entry) const {
|
||||
std::string title = "Secret trophy";
|
||||
|
||||
if (achievement_entry.IsUnlocked() || show_locked_info_ ||
|
||||
achievement_entry.flags &
|
||||
static_cast<uint32_t>(AchievementFlags::kShowUnachieved)) {
|
||||
title = xe::to_utf8(achievement_entry.name);
|
||||
title = xe::to_utf8(achievement_entry.achievement_name);
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
std::string GetAchievementDescription(
|
||||
const AchievementInfo& achievement_entry) {
|
||||
const Achievement& achievement_entry) const {
|
||||
std::string description = "Hidden description";
|
||||
|
||||
if (achievement_entry.flags &
|
||||
static_cast<uint32_t>(AchievementFlags::kShowUnachieved)) {
|
||||
description = xe::to_utf8(achievement_entry.unachieved);
|
||||
description = xe::to_utf8(achievement_entry.locked_description);
|
||||
}
|
||||
|
||||
if (achievement_entry.IsUnlocked() || show_locked_info_) {
|
||||
description = xe::to_utf8(achievement_entry.desc);
|
||||
description = xe::to_utf8(achievement_entry.unlocked_description);
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
ui::ImmediateTexture* GetIcon(const Achievement& achievement_entry) const {
|
||||
if (!achievement_entry.IsUnlocked() && !show_locked_info_) {
|
||||
return imgui_drawer()->GetLockedAchievementIcon();
|
||||
}
|
||||
|
||||
if (achievements_icons_.count(achievement_entry.image_id)) {
|
||||
return achievements_icons_.at(achievement_entry.image_id).get();
|
||||
}
|
||||
|
||||
if (achievement_entry.IsUnlocked()) {
|
||||
return nullptr;
|
||||
}
|
||||
return imgui_drawer()->GetLockedAchievementIcon();
|
||||
}
|
||||
|
||||
std::string GetUnlockedTime(const Achievement& achievement_entry) const {
|
||||
if (achievement_entry.IsUnlockedOnline()) {
|
||||
const auto unlock_time = chrono::WinSystemClock::to_local(
|
||||
achievement_entry.unlock_time.to_time_point());
|
||||
|
||||
return fmt::format("Unlocked: {:%Y-%m-%d %H:%M}", unlock_time);
|
||||
}
|
||||
|
||||
if (achievement_entry.unlock_time.is_valid()) {
|
||||
const auto unlock_time = chrono::WinSystemClock::to_local(
|
||||
achievement_entry.unlock_time.to_time_point());
|
||||
|
||||
return fmt::format("Unlocked: Offline ({:%Y-%m-%d %H:%M})", unlock_time);
|
||||
}
|
||||
return fmt::format("Unlocked: Offline");
|
||||
}
|
||||
|
||||
void DrawTitleAchievementInfo(ImGuiIO& io,
|
||||
const AchievementInfo& achievement_entry) {
|
||||
const Achievement& achievement_entry) const {
|
||||
const auto start_drawing_pos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
if (achievement_entry.IsUnlocked() || show_locked_info_) {
|
||||
if (achievements_icons_.count(achievement_entry.image_id)) {
|
||||
ImGui::Image(achievements_icons_.at(achievement_entry.image_id).get(),
|
||||
default_image_icon_size);
|
||||
} else {
|
||||
// Case when for whatever reason there is no icon available.
|
||||
ImGui::Image(0, default_image_icon_size);
|
||||
}
|
||||
} else {
|
||||
ImGui::Image(imgui_drawer()->GetLockedAchievementIcon(),
|
||||
default_image_icon_size);
|
||||
}
|
||||
|
||||
ImGui::Image(GetIcon(achievement_entry), default_image_icon_size);
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
ImGui::PushFont(imgui_drawer()->GetTitleFont());
|
||||
|
@ -654,13 +624,7 @@ class GameAchievementsDialog final : public XamDialog {
|
|||
ImGui::GetTextLineHeight());
|
||||
|
||||
if (achievement_entry.IsUnlocked()) {
|
||||
if (achievement_entry.IsUnlockedOnline()) {
|
||||
ImGui::TextUnformatted(fmt::format("Unlocked: {:%Y-%m-%d %H:%M}",
|
||||
achievement_entry.unlock_time)
|
||||
.c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(fmt::format("Unlocked: Locally").c_str());
|
||||
}
|
||||
ImGui::Text("%s", GetUnlockedTime(achievement_entry).c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
|
@ -691,11 +655,13 @@ class GameAchievementsDialog final : public XamDialog {
|
|||
|
||||
bool dialog_open = true;
|
||||
|
||||
if (!ImGui::Begin(
|
||||
fmt::format("{} Achievements List", title_info_.title_name).c_str(),
|
||||
&dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
if (!ImGui::Begin(fmt::format("{} Achievements List",
|
||||
xe::to_utf8(title_info_.title_name))
|
||||
.c_str(),
|
||||
&dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
Close();
|
||||
ImGui::End();
|
||||
return;
|
||||
|
@ -709,7 +675,7 @@ class GameAchievementsDialog final : public XamDialog {
|
|||
} else {
|
||||
if (ImGui::BeginTable("", 3,
|
||||
ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) {
|
||||
for (const auto& [_, entry] : achievements_info_) {
|
||||
for (const auto& entry : achievements_info_) {
|
||||
ImGui::TableNextRow(0, default_image_icon_size.y);
|
||||
DrawTitleAchievementInfo(io, entry);
|
||||
}
|
||||
|
@ -734,7 +700,7 @@ class GameAchievementsDialog final : public XamDialog {
|
|||
const TitleInfo title_info_;
|
||||
const UserProfile* profile_;
|
||||
|
||||
std::map<uint32_t, AchievementInfo> achievements_info_;
|
||||
std::vector<Achievement> achievements_info_;
|
||||
std::map<uint32_t, std::unique_ptr<ui::ImmediateTexture>> achievements_icons_;
|
||||
};
|
||||
|
||||
|
@ -745,6 +711,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
|
|||
: ui::ImGuiDialog(imgui_drawer),
|
||||
drawing_position_(drawing_position),
|
||||
profile_(profile),
|
||||
profile_manager_(kernel_state()->xam_state()->profile_manager()),
|
||||
dialog_name_(fmt::format("{}'s Games List", profile->name())) {
|
||||
LoadProfileGameInfo(imgui_drawer, profile);
|
||||
}
|
||||
|
@ -754,43 +721,20 @@ 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
void DrawTitleEntry(ImGuiIO& io, TitleInfo& entry) {
|
||||
const auto start_position = ImGui::GetCursorPos();
|
||||
const ImVec2 next_window_position =
|
||||
ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowSize().x + 20.f,
|
||||
|
@ -804,7 +748,7 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
|
|||
// Second Column
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushFont(imgui_drawer()->GetTitleFont());
|
||||
ImGui::TextUnformatted(entry.title_name.c_str());
|
||||
ImGui::TextUnformatted(xe::to_utf8(entry.title_name).c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::TextUnformatted(
|
||||
|
@ -816,18 +760,73 @@ 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");
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
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,
|
||||
if (ImGui::Selectable(fmt::format("##{:08X}Selectable", entry.id).c_str(),
|
||||
selected_title_ == entry.id,
|
||||
ImGuiSelectableFlags_SpanAllColumns,
|
||||
ImGui::GetContentRegionAvail())) {
|
||||
end_draw_position)) {
|
||||
selected_title_ = entry.id;
|
||||
new GameAchievementsDialog(imgui_drawer(), next_window_position, &entry,
|
||||
profile_);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem(
|
||||
fmt::format("Title Menu {:08X}", entry.id).c_str())) {
|
||||
selected_title_ = entry.id;
|
||||
if (ImGui::MenuItem("Refresh title stats", nullptr, nullptr, true)) {
|
||||
kernel_state()->xam_state()->user_tracker()->RefreshTitleSummary(
|
||||
profile_->xuid(), entry.id);
|
||||
|
||||
const auto title_info =
|
||||
kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo(
|
||||
profile_->xuid(), entry.id);
|
||||
|
||||
if (title_info) {
|
||||
entry = title_info.value();
|
||||
}
|
||||
}
|
||||
|
||||
const auto savefile_path = profile_manager_->GetProfileContentPath(
|
||||
profile_->xuid(), entry.id, XContentType::kSavedGame);
|
||||
|
||||
const auto dlc_path = profile_manager_->GetProfileContentPath(
|
||||
0, entry.id, XContentType::kMarketplaceContent);
|
||||
|
||||
const auto tu_path = profile_manager_->GetProfileContentPath(
|
||||
0, entry.id, XContentType::kInstaller);
|
||||
|
||||
if (ImGui::MenuItem("Open savefile directory", nullptr, nullptr,
|
||||
std::filesystem::exists(savefile_path))) {
|
||||
std::thread path_open(LaunchFileExplorer, savefile_path);
|
||||
path_open.detach();
|
||||
}
|
||||
if (ImGui::MenuItem("Open DLC directory", nullptr, nullptr,
|
||||
std::filesystem::exists(dlc_path))) {
|
||||
std::thread path_open(LaunchFileExplorer, dlc_path);
|
||||
path_open.detach();
|
||||
}
|
||||
if (ImGui::MenuItem("Open Title Update directory", nullptr, nullptr,
|
||||
std::filesystem::exists(tu_path))) {
|
||||
std::thread path_open(LaunchFileExplorer, tu_path);
|
||||
path_open.detach();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void OnDraw(ImGuiIO& io) override {
|
||||
|
@ -849,13 +848,30 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
|
|||
}
|
||||
|
||||
if (!info_.empty()) {
|
||||
if (info_.size() > 10) {
|
||||
ImGui::Text("Search: ");
|
||||
ImGui::SameLine();
|
||||
ImGui::InputText("##Search", title_name_filter_,
|
||||
title_name_filter_size);
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("", 2,
|
||||
ImGuiTableFlags_::ImGuiTableFlags_BordersInnerH)) {
|
||||
for (const auto& [_, entry] : info_) {
|
||||
ImGui::TableNextRow(0, default_image_icon_size.y);
|
||||
ImGui::TableNextRow(0, default_image_icon_size.y);
|
||||
for (auto& entry : info_) {
|
||||
std::string filter(title_name_filter_);
|
||||
if (!filter.empty()) {
|
||||
bool contains_filter =
|
||||
utf8::lower_ascii(xe::to_utf8(entry.title_name))
|
||||
.find(utf8::lower_ascii(filter)) != std::string::npos;
|
||||
|
||||
if (!contains_filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
DrawTitleEntry(io, entry);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
} else {
|
||||
|
@ -882,13 +898,18 @@ class GamesInfoDialog final : public ui::ImGuiDialog {
|
|||
}
|
||||
}
|
||||
|
||||
static constexpr uint8_t title_name_filter_size = 15;
|
||||
|
||||
std::string dialog_name_ = "";
|
||||
char title_name_filter_[title_name_filter_size] = "";
|
||||
uint32_t selected_title_ = 0;
|
||||
const ImVec2 drawing_position_ = {};
|
||||
|
||||
const UserProfile* profile_;
|
||||
const ProfileManager* profile_manager_;
|
||||
|
||||
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(
|
||||
|
@ -1543,6 +1564,7 @@ bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid,
|
|||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem("Profile Menu")) {
|
||||
*selected_xuid = xuid;
|
||||
if (user_index == XUserIndexAny) {
|
||||
if (ImGui::MenuItem("Login")) {
|
||||
profile_manager->Login(xuid);
|
||||
|
@ -1575,7 +1597,7 @@ bool xeDrawProfileContent(ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid,
|
|||
|
||||
const bool is_signedin = profile_manager->GetProfile(xuid) != nullptr;
|
||||
ImGui::BeginDisabled(!is_signedin);
|
||||
if (ImGui::MenuItem("Show Achievements")) {
|
||||
if (ImGui::MenuItem("Show Played Titles")) {
|
||||
new GamesInfoDialog(imgui_drawer, next_window_position,
|
||||
profile_manager->GetProfile(user_index));
|
||||
}
|
||||
|
@ -1922,20 +1944,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,19 +7,20 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#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"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
#include "third_party/stb/stb_image.h"
|
||||
|
||||
DECLARE_int32(user_language);
|
||||
|
||||
namespace xe {
|
||||
|
@ -207,22 +208,7 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
|
|||
be<uint32_t>* setting_ids, uint32_t unk,
|
||||
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());
|
||||
}
|
||||
}
|
||||
lpvoid_t overlapped_ptr) {
|
||||
assert_zero(unk); // probably flags
|
||||
|
||||
// must have at least 1 to 32 settings
|
||||
|
@ -270,109 +256,101 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
|
|||
return X_ERROR_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
auto run = [=](uint32_t& extended_error, uint32_t& length) {
|
||||
auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user_profile && !xuids) {
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
X_ERROR_NO_SUCH_USER);
|
||||
return X_ERROR_IO_PENDING;
|
||||
}
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
if (xuids) {
|
||||
uint64_t user_xuid = static_cast<uint64_t>(xuids[0]);
|
||||
if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) {
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
X_ERROR_NO_SUCH_USER);
|
||||
return X_ERROR_IO_PENDING;
|
||||
}
|
||||
if (!user_profile && !xuids) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid);
|
||||
}
|
||||
|
||||
if (!user_profile) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
// First call asks for size (fill buffer_size_ptr).
|
||||
// Second call asks for buffer contents with that size.
|
||||
|
||||
// TODO(gibbed): setting validity checking without needing a user profile
|
||||
// object.
|
||||
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;
|
||||
XELOGE(
|
||||
"xeXamUserReadProfileSettingsEx requested unimplemented setting "
|
||||
"{:08X}",
|
||||
setting_id);
|
||||
}
|
||||
}
|
||||
if (any_missing) {
|
||||
// TODO(benvanik): don't fail? most games don't even check!
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
X_ERROR_INVALID_PARAMETER);
|
||||
return X_ERROR_IO_PENDING;
|
||||
}
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
auto out_header = reinterpret_cast<X_USER_READ_PROFILE_SETTINGS*>(buffer);
|
||||
auto out_setting = reinterpret_cast<X_USER_PROFILE_SETTING*>(&out_header[1]);
|
||||
out_header->setting_count = static_cast<uint32_t>(setting_count);
|
||||
out_header->settings_ptr =
|
||||
kernel_state()->memory()->HostToGuestVirtual(out_setting);
|
||||
|
||||
DataByteStream out_stream(
|
||||
kernel_state()->memory()->HostToGuestVirtual(buffer), buffer, buffer_size,
|
||||
needed_header_size);
|
||||
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());
|
||||
if (xuids) {
|
||||
out_setting->xuid = user_profile->xuid();
|
||||
} else {
|
||||
out_setting->xuid = -1;
|
||||
out_setting->user_index = user_index;
|
||||
uint64_t user_xuid = static_cast<uint64_t>(xuids[0]);
|
||||
if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) {
|
||||
extended_error = X_HRESULT_FROM_WIN32(X_ERROR_NO_SUCH_USER);
|
||||
length = 0;
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid);
|
||||
}
|
||||
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);
|
||||
if (!user_profile) {
|
||||
extended_error = X_HRESULT_FROM_WIN32(X_ERROR_NO_SUCH_USER);
|
||||
length = 0;
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
++out_setting;
|
||||
|
||||
// First call asks for size (fill buffer_size_ptr).
|
||||
// Second call asks for buffer contents with that size.
|
||||
|
||||
bool any_missing = false;
|
||||
for (uint32_t i = 0; i < setting_count; ++i) {
|
||||
auto setting_id = static_cast<uint32_t>(setting_ids[i]);
|
||||
if (!UserSetting::is_setting_valid(setting_id)) {
|
||||
XELOGE(
|
||||
"xeXamUserReadProfileSettingsEx requested unimplemented "
|
||||
"setting "
|
||||
"{:08X}",
|
||||
setting_id);
|
||||
any_missing = true;
|
||||
}
|
||||
}
|
||||
if (any_missing) {
|
||||
extended_error = X_HRESULT_FROM_WIN32(X_ERROR_INVALID_PARAMETER);
|
||||
length = 0;
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
// TODO(benvanik): don't fail? most games don't even check!
|
||||
}
|
||||
|
||||
auto out_header = reinterpret_cast<X_USER_READ_PROFILE_SETTINGS*>(buffer);
|
||||
auto out_setting =
|
||||
reinterpret_cast<X_USER_PROFILE_SETTING*>(&out_header[1]);
|
||||
out_header->setting_count = static_cast<uint32_t>(setting_count);
|
||||
out_header->settings_ptr =
|
||||
kernel_state()->memory()->HostToGuestVirtual(out_setting);
|
||||
|
||||
uint32_t additional_data_buffer_ptr =
|
||||
out_header->settings_ptr +
|
||||
(setting_count * sizeof(X_USER_PROFILE_SETTING));
|
||||
|
||||
std::fill_n(out_setting, setting_count, X_USER_PROFILE_SETTING{});
|
||||
|
||||
for (uint32_t n = 0; n < setting_count; ++n) {
|
||||
uint32_t setting_id = setting_ids[n];
|
||||
|
||||
const bool is_valid =
|
||||
kernel_state()->xam_state()->user_tracker()->GetUserSetting(
|
||||
user_profile->xuid(),
|
||||
title_id ? title_id : kernel_state()->title_id(), setting_id,
|
||||
out_setting, additional_data_buffer_ptr);
|
||||
|
||||
if (is_valid) {
|
||||
if (xuids) {
|
||||
out_setting->xuid = user_profile->xuid();
|
||||
} else {
|
||||
out_setting->user_index = user_index;
|
||||
}
|
||||
}
|
||||
++out_setting;
|
||||
}
|
||||
|
||||
extended_error = X_HRESULT_FROM_WIN32(X_STATUS_SUCCESS);
|
||||
length = 0;
|
||||
return X_STATUS_SUCCESS;
|
||||
};
|
||||
|
||||
if (!overlapped_ptr) {
|
||||
uint32_t extended_error, length;
|
||||
return run(extended_error, length);
|
||||
}
|
||||
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
X_ERROR_SUCCESS);
|
||||
return X_ERROR_IO_PENDING;
|
||||
}
|
||||
return X_ERROR_SUCCESS;
|
||||
kernel_state()->CompleteOverlappedDeferredEx(run, overlapped_ptr);
|
||||
return X_ERROR_IO_PENDING;
|
||||
}
|
||||
|
||||
dword_result_t XamUserReadProfileSettings_entry(
|
||||
dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids,
|
||||
dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr,
|
||||
lpvoid_t buffer_ptr, pointer_t<XAM_OVERLAPPED> overlapped) {
|
||||
lpvoid_t buffer_ptr, lpvoid_t overlapped) {
|
||||
return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids,
|
||||
setting_count, setting_ids, 0,
|
||||
buffer_size_ptr, buffer_ptr, overlapped);
|
||||
|
@ -382,7 +360,7 @@ DECLARE_XAM_EXPORT1(XamUserReadProfileSettings, kUserProfiles, kImplemented);
|
|||
dword_result_t XamUserReadProfileSettingsEx_entry(
|
||||
dword_t title_id, dword_t user_index, dword_t xuid_count, lpqword_t xuids,
|
||||
dword_t setting_count, lpdword_t setting_ids, lpdword_t buffer_size_ptr,
|
||||
dword_t unk_2, lpvoid_t buffer_ptr, pointer_t<XAM_OVERLAPPED> overlapped) {
|
||||
dword_t unk_2, lpvoid_t buffer_ptr, lpvoid_t overlapped) {
|
||||
return XamUserReadProfileSettingsEx(title_id, user_index, xuid_count, xuids,
|
||||
setting_count, setting_ids, unk_2,
|
||||
buffer_size_ptr, buffer_ptr, overlapped);
|
||||
|
@ -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_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);
|
||||
}
|
||||
|
@ -692,18 +624,57 @@ dword_result_t XamParseGamerTileKey_entry(lpdword_t key_ptr, lpdword_t out1_ptr,
|
|||
}
|
||||
DECLARE_XAM_EXPORT1(XamParseGamerTileKey, kUserProfiles, kStub);
|
||||
|
||||
dword_result_t XamReadTileToTexture_entry(dword_t unknown, dword_t title_id,
|
||||
dword_result_t XamReadTileToTexture_entry(dword_t tile_type, dword_t title_id,
|
||||
qword_t tile_id, dword_t user_index,
|
||||
lpvoid_t buffer_ptr, dword_t stride,
|
||||
dword_t height,
|
||||
dword_t tile_height,
|
||||
dword_t overlapped_ptr) {
|
||||
// TODO(gibbed): unknown=0,2,3,9
|
||||
if (!tile_id) {
|
||||
if (!buffer_ptr) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
size_t size = size_t(stride) * size_t(height);
|
||||
std::memset(buffer_ptr, 0xFF, size);
|
||||
size_t buffer_size = size_t(stride) * size_t(tile_height);
|
||||
|
||||
auto user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
if (!user) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
std::span<const uint8_t> tile =
|
||||
kernel_state()->xam_state()->user_tracker()->GetIcon(
|
||||
user->xuid(), title_id, static_cast<XTileType>(tile_type.value()),
|
||||
tile_id);
|
||||
|
||||
if (tile.empty()) {
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
int width, height, channels;
|
||||
unsigned char* imageData =
|
||||
stbi_load_from_memory(tile.data(), static_cast<int>(tile.size()), &width,
|
||||
&height, &channels, STBI_rgb_alpha);
|
||||
|
||||
size_t icon_dimmension_size = width * height;
|
||||
std::fill_n(reinterpret_cast<uint8_t*>(buffer_ptr.host_address()),
|
||||
icon_dimmension_size * sizeof(uint32_t), 0);
|
||||
|
||||
for (int i = 0; i < icon_dimmension_size; i++) {
|
||||
unsigned char* pixel = &imageData[i * sizeof(uint32_t)];
|
||||
|
||||
// RGBA to ARGB. TODO: Find faster method!
|
||||
// RGBA->AGBR
|
||||
std::swap(pixel[0], pixel[3]);
|
||||
// AGBR->ARBG
|
||||
std::swap(pixel[1], pixel[3]);
|
||||
// ARBG->ARGB
|
||||
std::swap(pixel[2], pixel[3]);
|
||||
}
|
||||
|
||||
memcpy(buffer_ptr, imageData,
|
||||
std::min(buffer_size, static_cast<size_t>(icon_dimmension_size *
|
||||
sizeof(uint32_t))));
|
||||
|
||||
stbi_image_free(imageData);
|
||||
|
||||
if (overlapped_ptr) {
|
||||
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr,
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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(uint64_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) {
|
||||
X_XDBF_GPD_SETTING_HEADER* setting = GetSetting(id);
|
||||
|
||||
if (!setting) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (setting->setting_type != X_USER_DATA_TYPE::BINARY &&
|
||||
setting->setting_type != X_USER_DATA_TYPE::WSTRING) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uint32_t size = setting->base_data.binary.size;
|
||||
const uint8_t* data_ptr = reinterpret_cast<uint8_t*>(setting + 1);
|
||||
return {data_ptr, size};
|
||||
}
|
||||
|
||||
void GpdInfo::UpsertSetting(const UserSetting* setting_data) {
|
||||
const auto serialized_data = setting_data->Serialize();
|
||||
|
||||
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);
|
||||
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,158 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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/xam/user_data.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
|
||||
};
|
||||
|
||||
enum class AchievementFlags : uint32_t {
|
||||
kTypeMask = 0x7,
|
||||
kShowUnachieved = 0x8,
|
||||
kAchievedOnline = 0x10000,
|
||||
kAchieved = 0x20000,
|
||||
kNotAchievable = 0x40000,
|
||||
kWasNotAchievable = 0x80000,
|
||||
kPlatformMask = 0x700000,
|
||||
kColorizable = 0x1000000, // avatar awards 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;
|
||||
|
||||
bool is_achievement_unlocked() const {
|
||||
return flags & static_cast<uint32_t>(AchievementFlags::kAchieved);
|
||||
}
|
||||
};
|
||||
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.
|
||||
X_FILETIME 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];
|
||||
X_USER_DATA_UNION 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);
|
||||
|
||||
// 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(uint64_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:
|
||||
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,114 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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) {
|
||||
X_XDBF_GPD_TITLE_PLAYED* current_info = GetTitleInfo(title_id);
|
||||
if (!current_info) {
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(current_info, title_data, sizeof(X_XDBF_GPD_TITLE_PLAYED));
|
||||
}
|
||||
|
||||
} // 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,196 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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"
|
||||
#include <ranges>
|
||||
|
||||
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;
|
||||
|
||||
auto achievements =
|
||||
entries_ | std::views::filter([](const auto& entry) {
|
||||
return !IsSyncEntry(&entry);
|
||||
}) |
|
||||
std::views::filter([](const auto& entry) {
|
||||
return IsEntryOfSection(&entry, GpdSection::kAchievement);
|
||||
});
|
||||
|
||||
for (const auto& achievement : achievements) {
|
||||
ids.push_back(static_cast<uint32_t>(achievement.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);
|
||||
}
|
||||
|
||||
uint32_t GpdInfoTitle::GetTotalGamerscore() {
|
||||
const auto ids = GetAchievementsIds();
|
||||
|
||||
uint32_t gamerscore = 0;
|
||||
for (const auto id : ids) {
|
||||
gamerscore += GetAchievementEntry(id)->gamerscore;
|
||||
}
|
||||
|
||||
return gamerscore;
|
||||
}
|
||||
uint32_t GpdInfoTitle::GetGamerscore() {
|
||||
const auto ids = GetAchievementsIds();
|
||||
uint32_t gamerscore = 0;
|
||||
for (const auto id : ids) {
|
||||
const auto entry = GetAchievementEntry(id);
|
||||
if (entry->is_achievement_unlocked()) {
|
||||
gamerscore += GetAchievementEntry(id)->gamerscore;
|
||||
}
|
||||
}
|
||||
return gamerscore;
|
||||
}
|
||||
|
||||
uint32_t GpdInfoTitle::GetAchievementCount() {
|
||||
return static_cast<uint32_t>(GetAchievementsIds().size());
|
||||
}
|
||||
|
||||
uint32_t GpdInfoTitle::GetUnlockedAchievementCount() {
|
||||
const auto ids = GetAchievementsIds();
|
||||
uint32_t count = 0;
|
||||
for (const auto id : ids) {
|
||||
const auto entry = GetAchievementEntry(id);
|
||||
if (entry->is_achievement_unlocked()) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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);
|
||||
|
||||
uint32_t GetTotalGamerscore();
|
||||
uint32_t GetGamerscore();
|
||||
uint32_t GetAchievementCount();
|
||||
uint32_t GetUnlockedAchievementCount();
|
||||
|
||||
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,300 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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"
|
||||
|
||||
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) {
|
||||
return GetSpaEntry<const AchievementTableEntry*>(achievements_, id);
|
||||
}
|
||||
|
||||
const XdbfContextTableEntry* SpaInfo::GetContext(uint32_t id) {
|
||||
return GetSpaEntry<const XdbfContextTableEntry*>(contexts_, id);
|
||||
}
|
||||
|
||||
const XdbfPropertyTableEntry* SpaInfo::GetProperty(uint32_t id) {
|
||||
return GetSpaEntry<const XdbfPropertyTableEntry*>(properties_, id);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T SpaInfo::GetSpaEntry(std::vector<T>& container, uint32_t id) {
|
||||
for (const auto& entry : container) {
|
||||
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,223 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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 <map>
|
||||
#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();
|
||||
|
||||
template <typename T>
|
||||
static T GetSpaEntry(std::vector<T>& container, uint32_t id);
|
||||
};
|
||||
|
||||
} // 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;
|
||||
};
|
||||
|
||||
|
|
|
@ -245,10 +245,13 @@ std::map<uint32_t, std::unique_ptr<ImmediateTexture>> ImGuiDrawer::LoadIcons(
|
|||
int width, height, channels;
|
||||
|
||||
for (const auto& icon : data) {
|
||||
unsigned char* image_data =
|
||||
stbi_load_from_memory(icon.second.first, icon.second.second, &width,
|
||||
&height, &channels, STBI_rgb_alpha);
|
||||
unsigned char* image_data = stbi_load_from_memory(
|
||||
icon.second.data(), static_cast<int>(icon.second.size()), &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)));
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "third_party/imgui/imgui.h"
|
||||
|
@ -34,7 +35,7 @@ class ImGuiDialog;
|
|||
class ImGuiNotification;
|
||||
class Window;
|
||||
|
||||
using IconsData = std::map<uint32_t, std::pair<const uint8_t*, uint32_t>>;
|
||||
using IconsData = std::map<uint32_t, std::span<const uint8_t>>;
|
||||
|
||||
class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
||||
public:
|
||||
|
|
|
@ -51,5 +51,6 @@ std::unique_ptr<MappedMemory> DiscImageEntry::OpenMapped(
|
|||
return mmap_->Slice(real_offset, real_length);
|
||||
}
|
||||
|
||||
bool DiscImageEntry::DeleteEntryInternal(Entry* entry) { return false; }
|
||||
} // namespace vfs
|
||||
} // namespace xe
|
||||
|
|
|
@ -45,6 +45,8 @@ class DiscImageEntry : public Entry {
|
|||
private:
|
||||
friend class DiscImageDevice;
|
||||
|
||||
bool DeleteEntryInternal(Entry* entry) override;
|
||||
|
||||
MappedMemory* mmap_;
|
||||
size_t data_offset_;
|
||||
size_t data_size_;
|
||||
|
|
|
@ -45,5 +45,7 @@ std::unique_ptr<MappedMemory> DiscZarchiveEntry::OpenMapped(
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
bool DiscZarchiveEntry::DeleteEntryInternal(Entry* entry) { return false; }
|
||||
|
||||
} // namespace vfs
|
||||
} // namespace xe
|
||||
|
|
|
@ -45,6 +45,8 @@ class DiscZarchiveEntry : public Entry {
|
|||
friend class DiscZarchiveDevice;
|
||||
friend class DiscZarchiveFile;
|
||||
|
||||
bool DeleteEntryInternal(Entry* entry) override;
|
||||
|
||||
uint32_t handle_;
|
||||
size_t data_offset_;
|
||||
size_t data_size_;
|
||||
|
|
|
@ -104,10 +104,20 @@ bool HostPathEntry::DeleteEntryInternal(Entry* entry) {
|
|||
auto removed = std::filesystem::remove_all(full_path, ec);
|
||||
return removed >= 1 && removed != static_cast<std::uintmax_t>(-1);
|
||||
} else {
|
||||
// Delete file only if it exists.
|
||||
return !std::filesystem::is_directory(full_path) &&
|
||||
(!std::filesystem::exists(full_path) ||
|
||||
std::filesystem::remove(full_path, ec));
|
||||
// Skip directories, they we're handled above.
|
||||
if (std::filesystem::is_directory(full_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(full_path)) {
|
||||
const auto result = std::filesystem::remove(full_path, ec);
|
||||
if (ec) {
|
||||
XELOGE("{}: Cannot remove file entry. File: {} Error: {}", __func__,
|
||||
full_path, ec.message());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,5 +51,7 @@ X_STATUS NullEntry::Open(uint32_t desired_access, File** out_file) {
|
|||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
bool NullEntry::DeleteEntryInternal(Entry* entry) { return false; }
|
||||
|
||||
} // namespace vfs
|
||||
} // namespace xe
|
||||
|
|
|
@ -34,6 +34,8 @@ class NullEntry : public Entry {
|
|||
|
||||
private:
|
||||
friend class NullDevice;
|
||||
|
||||
bool DeleteEntryInternal(Entry* entry) override;
|
||||
};
|
||||
|
||||
} // namespace vfs
|
||||
|
|
|
@ -42,5 +42,7 @@ X_STATUS XContentContainerEntry::Open(uint32_t desired_access,
|
|||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
bool XContentContainerEntry::DeleteEntryInternal(Entry* entry) { return false; }
|
||||
|
||||
} // namespace vfs
|
||||
} // namespace xe
|
|
@ -51,6 +51,8 @@ class XContentContainerEntry : public Entry {
|
|||
friend class StfsContainerDevice;
|
||||
friend class SvodContainerDevice;
|
||||
|
||||
bool DeleteEntryInternal(Entry* entry) override;
|
||||
|
||||
MultiFileHandles* files_;
|
||||
size_t data_offset_;
|
||||
size_t data_size_;
|
||||
|
|
|
@ -140,7 +140,7 @@ class Entry {
|
|||
const std::string_view name, uint32_t attributes) {
|
||||
return nullptr;
|
||||
}
|
||||
virtual bool DeleteEntryInternal(Entry* entry) { return false; }
|
||||
virtual bool DeleteEntryInternal(Entry* entry) = 0;
|
||||
virtual void RenameEntryInternal(const std::filesystem::path file_path) {}
|
||||
|
||||
xe::global_critical_region global_critical_region_;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "xenia/base/chrono.h"
|
||||
#include "xenia/base/memory.h"
|
||||
#include "xenia/base/string.h"
|
||||
|
||||
|
@ -463,6 +464,48 @@ struct X_KSPINLOCK {
|
|||
xe::be<uint32_t> prcb_of_owner;
|
||||
};
|
||||
static_assert_size(X_KSPINLOCK, 4);
|
||||
|
||||
struct X_FILETIME {
|
||||
static constexpr uint64_t minimal_valid_time = 125911584000000000;
|
||||
static constexpr uint64_t maximal_valid_time = 157469184000000000;
|
||||
|
||||
xe::be<uint32_t> high_part;
|
||||
xe::be<uint32_t> low_part;
|
||||
|
||||
X_FILETIME() {
|
||||
high_part = 0;
|
||||
low_part = 0;
|
||||
}
|
||||
|
||||
X_FILETIME(uint64_t filetime) {
|
||||
high_part = static_cast<uint32_t>(filetime >> 32);
|
||||
low_part = static_cast<uint32_t>(filetime);
|
||||
}
|
||||
|
||||
X_FILETIME(std::time_t time) {
|
||||
const auto file_time =
|
||||
chrono::WinSystemClock::to_file_time(chrono::WinSystemClock::from_sys(
|
||||
std::chrono::system_clock::from_time_t(time)));
|
||||
|
||||
high_part = static_cast<uint32_t>(file_time >> 32);
|
||||
low_part = static_cast<uint32_t>(file_time);
|
||||
}
|
||||
|
||||
chrono::WinSystemClock::time_point to_time_point() const {
|
||||
const uint64_t filetime =
|
||||
(static_cast<uint64_t>(high_part) << 32) | low_part;
|
||||
|
||||
return chrono::WinSystemClock::from_file_time(filetime);
|
||||
}
|
||||
|
||||
bool is_valid() const {
|
||||
const uint64_t filetime =
|
||||
(static_cast<uint64_t>(high_part) << 32) | low_part;
|
||||
|
||||
return filetime >= minimal_valid_time && filetime <= maximal_valid_time;
|
||||
}
|
||||
};
|
||||
static_assert_size(X_FILETIME, 0x8);
|
||||
#pragma pack(pop)
|
||||
|
||||
// Found by dumping the kSectionStringTable sections of various games:
|
||||
|
@ -487,6 +530,7 @@ enum class XLanguage : uint32_t {
|
|||
};
|
||||
|
||||
enum class XContentType : uint32_t {
|
||||
kInvalid = 0x00000000,
|
||||
kSavedGame = 0x00000001,
|
||||
kMarketplaceContent = 0x00000002,
|
||||
kPublisher = 0x00000003,
|
||||
|
|
Loading…
Reference in New Issue