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