From 2a5ab07024168d8470b6dd875d0168f4fd5cd461 Mon Sep 17 00:00:00 2001 From: emoose Date: Fri, 16 Nov 2018 05:00:07 +0000 Subject: [PATCH] [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. --- src/xenia/emulator.cc | 4 +- src/xenia/kernel/util/xdbf_utils.cc | 15 ++- src/xenia/kernel/util/xdbf_utils.h | 21 +++- src/xenia/kernel/xam/user_profile.cc | 175 +++++++++++++++++++++++++++ src/xenia/kernel/xam/user_profile.h | 13 ++ 5 files changed, 225 insertions(+), 3 deletions(-) diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 104de7318..09fe4dd63 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -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(), diff --git a/src/xenia/kernel/util/xdbf_utils.cc b/src/xenia/kernel/util/xdbf_utils.cc index 5f7ff9672..4af86cb8f 100644 --- a/src/xenia/kernel/util/xdbf_utils.cc +++ b/src/xenia/kernel/util/xdbf_utils.cc @@ -244,11 +244,24 @@ XdbfLocale SpaFile::GetDefaultLocale() const { return static_cast(static_cast(xstc->default_language)); } -std::string SpaFile::GetTitle() const { +std::string SpaFile::GetTitleName() const { return GetStringTableEntry(GetDefaultLocale(), static_cast(XdbfSpaID::Title)); } +uint32_t SpaFile::GetTitleId() const { + auto block = GetEntry(static_cast(XdbfSpaSection::kMetadata), + static_cast(XdbfSpaID::Xthd)); + if (!block) { + return -1; + } + + auto xthd = reinterpret_cast(block->data.data()); + assert_true(xthd->magic == static_cast(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]; diff --git a/src/xenia/kernel/util/xdbf_utils.h b/src/xenia/kernel/util/xdbf_utils.h index 6f9c3b321..7640f1186 100644 --- a/src/xenia/kernel/util/xdbf_utils.h +++ b/src/xenia/kernel/util/xdbf_utils.h @@ -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 magic; + xe::be version; + xe::be unk8; + xe::be title_id; + xe::be unk10; // always 1? + xe::be title_version_major; + xe::be title_version_minor; + xe::be title_version_build; + xe::be title_version_revision; + xe::be unk1C; + xe::be unk20; + xe::be unk24; + xe::be unk28; +}; +static_assert_size(X_XDBF_XTHD_DATA, 0x2C); + struct X_XDBF_TABLE_HEADER { xe::be magic; xe::be 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 { diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 6471b3d1e..0230f2b2d 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -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(0x63E83FFE)); // XPROFILE_TITLE_SPECIFIC3 AddSetting(std::make_unique(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 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 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(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 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) { diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index f8f2dacd8..e6223fe34 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -15,6 +15,7 @@ #include #include +#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* 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> setting_list_; @@ -214,6 +223,10 @@ class UserProfile { void LoadSetting(UserProfile::Setting*); void SaveSetting(UserProfile::Setting*); + + std::unordered_map title_gpds_; + util::GpdFile dash_gpd_; + util::GpdFile* curr_gpd_ = nullptr; }; } // namespace xam