[Kernel] Let UserProfile load/save GPDs, convert SPA -> GPD on XEX load
UserProfile will now try loading dash GPD + any game GPDs from the "profile" folder when initialized. After loading an XEX the title's SPA data gets passed to UserProfile, which will then either set current GPD based on title ID in the SPA, or create new GPD and copy achievements/images over to it.
This commit is contained in:
parent
734bbc7515
commit
2a5ab07024
|
@ -665,7 +665,9 @@ X_STATUS Emulator::CompleteLaunch(const std::wstring& path,
|
|||
kernel::util::SpaFile spa;
|
||||
if (spa.Read(module->memory()->TranslateVirtual(resource_data),
|
||||
resource_size)) {
|
||||
game_title_ = xe::to_wstring(spa.GetTitle());
|
||||
// Set title SPA and get title name/icon
|
||||
kernel_state_->user_profile()->SetTitleSpaData(spa);
|
||||
game_title_ = xe::to_wstring(spa.GetTitleName());
|
||||
auto icon_block = spa.GetIcon();
|
||||
if (icon_block) {
|
||||
display_window_->SetIcon(icon_block->data.data(),
|
||||
|
|
|
@ -244,11 +244,24 @@ XdbfLocale SpaFile::GetDefaultLocale() const {
|
|||
return static_cast<XdbfLocale>(static_cast<uint32_t>(xstc->default_language));
|
||||
}
|
||||
|
||||
std::string SpaFile::GetTitle() const {
|
||||
std::string SpaFile::GetTitleName() const {
|
||||
return GetStringTableEntry(GetDefaultLocale(),
|
||||
static_cast<uint16_t>(XdbfSpaID::Title));
|
||||
}
|
||||
|
||||
uint32_t SpaFile::GetTitleId() const {
|
||||
auto block = GetEntry(static_cast<uint16_t>(XdbfSpaSection::kMetadata),
|
||||
static_cast<uint64_t>(XdbfSpaID::Xthd));
|
||||
if (!block) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto xthd = reinterpret_cast<const X_XDBF_XTHD_DATA*>(block->data.data());
|
||||
assert_true(xthd->magic == static_cast<uint32_t>(XdbfSpaID::Xthd));
|
||||
|
||||
return xthd->title_id;
|
||||
}
|
||||
|
||||
bool GpdFile::GetAchievement(uint16_t id, XdbfAchievement* dest) {
|
||||
for (size_t i = 0; i < entries.size(); i++) {
|
||||
auto* entry = (XdbfEntry*)&entries[i];
|
||||
|
|
|
@ -26,6 +26,7 @@ enum class XdbfSpaID : uint64_t {
|
|||
Xach = 'XACH',
|
||||
Xstr = 'XSTR',
|
||||
Xstc = 'XSTC',
|
||||
Xthd = 'XTHD',
|
||||
Title = 0x8000,
|
||||
};
|
||||
|
||||
|
@ -96,6 +97,23 @@ struct X_XDBF_XSTC_DATA {
|
|||
};
|
||||
static_assert_size(X_XDBF_XSTC_DATA, 16);
|
||||
|
||||
struct X_XDBF_XTHD_DATA {
|
||||
xe::be<uint32_t> magic;
|
||||
xe::be<uint32_t> version;
|
||||
xe::be<uint32_t> unk8;
|
||||
xe::be<uint32_t> title_id;
|
||||
xe::be<uint32_t> unk10; // always 1?
|
||||
xe::be<uint16_t> title_version_major;
|
||||
xe::be<uint16_t> title_version_minor;
|
||||
xe::be<uint16_t> title_version_build;
|
||||
xe::be<uint16_t> title_version_revision;
|
||||
xe::be<uint32_t> unk1C;
|
||||
xe::be<uint32_t> unk20;
|
||||
xe::be<uint32_t> unk24;
|
||||
xe::be<uint32_t> unk28;
|
||||
};
|
||||
static_assert_size(X_XDBF_XTHD_DATA, 0x2C);
|
||||
|
||||
struct X_XDBF_TABLE_HEADER {
|
||||
xe::be<uint32_t> magic;
|
||||
xe::be<uint32_t> version;
|
||||
|
@ -324,7 +342,8 @@ class SpaFile : public XdbfFile {
|
|||
|
||||
XdbfEntry* GetIcon() const;
|
||||
XdbfLocale GetDefaultLocale() const;
|
||||
std::string GetTitle() const;
|
||||
std::string GetTitleName() const;
|
||||
uint32_t GetTitleId() const;
|
||||
};
|
||||
|
||||
class GpdFile : public XdbfFile {
|
||||
|
|
|
@ -12,11 +12,16 @@
|
|||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
#include "xenia/kernel/xam/user_profile.h"
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/mapped_memory.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
constexpr uint32_t kDashboardID = 0xFFFE07D1;
|
||||
|
||||
UserProfile::UserProfile() {
|
||||
xuid_ = 0xBABEBABEBABEBABE;
|
||||
name_ = "User";
|
||||
|
@ -85,6 +90,176 @@ UserProfile::UserProfile() {
|
|||
AddSetting(std::make_unique<BinarySetting>(0x63E83FFE));
|
||||
// XPROFILE_TITLE_SPECIFIC3
|
||||
AddSetting(std::make_unique<BinarySetting>(0x63E83FFD));
|
||||
|
||||
// Try loading profile GPD files...
|
||||
LoadGpdFiles();
|
||||
}
|
||||
|
||||
void UserProfile::LoadGpdFiles() {
|
||||
auto mmap_ =
|
||||
MappedMemory::Open(L"profile\\FFFE07D1.gpd", MappedMemory::Mode::kRead);
|
||||
if (!mmap_) {
|
||||
XELOGW("Dash GPD not found, using blank one");
|
||||
return;
|
||||
}
|
||||
|
||||
dash_gpd_.Read(mmap_->data(), mmap_->size());
|
||||
mmap_->Close();
|
||||
|
||||
std::vector<util::XdbfTitlePlayed> titles;
|
||||
dash_gpd_.GetTitles(&titles);
|
||||
|
||||
for (auto title : titles) {
|
||||
wchar_t fname[256];
|
||||
_swprintf(fname, L"profile\\%X.gpd", title.title_id);
|
||||
mmap_ = MappedMemory::Open(fname, MappedMemory::Mode::kRead);
|
||||
if (!mmap_) {
|
||||
XELOGE("GPD for title %X (%ws) not found!", title.title_id,
|
||||
title.title_name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
util::GpdFile title_gpd;
|
||||
title_gpd.Read(mmap_->data(), mmap_->size());
|
||||
mmap_->Close();
|
||||
|
||||
title_gpds_[title.title_id] = title_gpd;
|
||||
}
|
||||
}
|
||||
|
||||
util::GpdFile* UserProfile::SetTitleSpaData(const util::SpaFile& spa_data) {
|
||||
uint32_t spa_title = spa_data.GetTitleId();
|
||||
|
||||
auto gpd = title_gpds_.find(spa_title);
|
||||
|
||||
if (gpd == title_gpds_.end()) {
|
||||
// GPD not found... have to create it!
|
||||
XELOGD("Creating new GPD for title %X", spa_title);
|
||||
|
||||
util::XdbfTitlePlayed title_info;
|
||||
title_info.title_name = xe::to_wstring(spa_data.GetTitleName());
|
||||
title_info.title_id = spa_title;
|
||||
|
||||
std::vector<util::XdbfAchievement> spa_achievements;
|
||||
// TODO: let user choose locale?
|
||||
spa_data.GetAchievements(spa_data.GetDefaultLocale(), &spa_achievements);
|
||||
|
||||
// Copy cheevos from SPA -> GPD
|
||||
util::GpdFile title_gpd;
|
||||
for (auto ach : spa_achievements) {
|
||||
title_gpd.UpdateAchievement(ach);
|
||||
|
||||
title_info.achievements_possible++;
|
||||
title_info.gamerscore_total += ach.gamerscore;
|
||||
}
|
||||
|
||||
// Try copying achievement images if we can...
|
||||
for (auto ach : spa_achievements) {
|
||||
auto* image_entry = spa_data.GetEntry(
|
||||
static_cast<uint16_t>(util::XdbfSpaSection::kImage), ach.image_id);
|
||||
if (image_entry) {
|
||||
title_gpd.UpdateEntry(*image_entry);
|
||||
}
|
||||
}
|
||||
|
||||
title_gpds_[spa_title] = title_gpd;
|
||||
|
||||
// Update dash GPD with title and write updated GPDs
|
||||
dash_gpd_.UpdateTitle(title_info);
|
||||
|
||||
UpdateGpd(spa_title, title_gpd);
|
||||
UpdateGpd(kDashboardID, dash_gpd_);
|
||||
}
|
||||
|
||||
// TODO: check SPA for any achievements current GPD might be missing
|
||||
// (maybe added in TUs etc?)
|
||||
|
||||
curr_gpd_ = &title_gpds_[spa_title];
|
||||
return curr_gpd_;
|
||||
}
|
||||
|
||||
bool UserProfile::UpdateGpdFiles() {
|
||||
// TODO: optimize so we only have to update the current title?
|
||||
for (const auto& pair : title_gpds_) {
|
||||
auto gpd = pair.second;
|
||||
bool result = UpdateGpd(pair.first, gpd);
|
||||
if (!result) {
|
||||
XELOGE("UpdateGpdFiles failed on title %X...", pair.first);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// No need to update dash GPD here, the UpdateGpd func should take care of it
|
||||
// when needed
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UserProfile::UpdateGpd(uint32_t title_id, util::GpdFile& gpd_data) {
|
||||
size_t gpd_length = 0;
|
||||
if (!gpd_data.Write(nullptr, &gpd_length)) {
|
||||
XELOGE("Failed to get GPD size for %X!", title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filesystem::PathExists(L"profile\\")) {
|
||||
filesystem::CreateFolder(L"profile\\");
|
||||
}
|
||||
|
||||
wchar_t fname[256];
|
||||
_swprintf(fname, L"profile\\%X.gpd", title_id);
|
||||
|
||||
filesystem::CreateFile(fname);
|
||||
auto mmap_ =
|
||||
MappedMemory::Open(fname, MappedMemory::Mode::kReadWrite, 0, gpd_length);
|
||||
if (!mmap_) {
|
||||
XELOGE("Failed to open %X.gpd for writing!", title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret_val = true;
|
||||
|
||||
if (!gpd_data.Write(mmap_->data(), &gpd_length)) {
|
||||
XELOGE("Failed to write GPD data for %X!", title_id);
|
||||
ret_val = false;
|
||||
} else {
|
||||
// Check if we need to update dashboard data...
|
||||
if (title_id != kDashboardID) {
|
||||
util::XdbfTitlePlayed title_info;
|
||||
if (dash_gpd_.GetTitle(title_id, &title_info)) {
|
||||
std::vector<util::XdbfAchievement> gpd_achievements;
|
||||
// TODO: let user choose locale?
|
||||
gpd_data.GetAchievements(&gpd_achievements);
|
||||
uint32_t num_ach_total = 0;
|
||||
uint32_t num_ach_earned = 0;
|
||||
uint32_t gamerscore_total = 0;
|
||||
uint32_t gamerscore_earned = 0;
|
||||
for (auto ach : gpd_achievements) {
|
||||
num_ach_total++;
|
||||
gamerscore_total += ach.gamerscore;
|
||||
if (ach.IsUnlocked()) {
|
||||
num_ach_earned++;
|
||||
gamerscore_earned += ach.gamerscore;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_ach_total != title_info.achievements_possible ||
|
||||
num_ach_earned != title_info.achievements_earned ||
|
||||
gamerscore_total != title_info.gamerscore_total ||
|
||||
gamerscore_earned != title_info.gamerscore_earned) {
|
||||
title_info.achievements_possible = num_ach_total;
|
||||
title_info.achievements_earned = num_ach_earned;
|
||||
title_info.gamerscore_total = gamerscore_total;
|
||||
title_info.gamerscore_earned = gamerscore_earned;
|
||||
|
||||
dash_gpd_.UpdateTitle(title_info);
|
||||
UpdateGpd(kDashboardID, dash_gpd_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mmap_->Close(gpd_length);
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
void UserProfile::AddSetting(std::unique_ptr<Setting> setting) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/kernel/util/xdbf_utils.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
|
@ -206,7 +207,15 @@ class UserProfile {
|
|||
void AddSetting(std::unique_ptr<Setting> setting);
|
||||
Setting* GetSetting(uint32_t setting_id);
|
||||
|
||||
util::GpdFile* SetTitleSpaData(const util::SpaFile& spa_data);
|
||||
util::GpdFile* GetTitleGpd() { return curr_gpd_; }
|
||||
|
||||
bool UpdateGpdFiles();
|
||||
|
||||
private:
|
||||
void LoadGpdFiles();
|
||||
bool UpdateGpd(uint32_t title_id, util::GpdFile& gpd_data);
|
||||
|
||||
uint64_t xuid_;
|
||||
std::string name_;
|
||||
std::vector<std::unique_ptr<Setting>> setting_list_;
|
||||
|
@ -214,6 +223,10 @@ class UserProfile {
|
|||
|
||||
void LoadSetting(UserProfile::Setting*);
|
||||
void SaveSetting(UserProfile::Setting*);
|
||||
|
||||
std::unordered_map<uint32_t, util::GpdFile> title_gpds_;
|
||||
util::GpdFile dash_gpd_;
|
||||
util::GpdFile* curr_gpd_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
|
|
Loading…
Reference in New Issue