[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:
emoose 2018-11-16 05:00:07 +00:00
parent 734bbc7515
commit 2a5ab07024
No known key found for this signature in database
GPG Key ID: 3735C67912F5FF97
5 changed files with 225 additions and 3 deletions

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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