diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 161c6cdd3..cec87fdfc 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -35,8 +35,8 @@ #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" #include "xenia/kernel/util/gameinfo_utils.h" -#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/xam/xam_module.h" +#include "xenia/kernel/xam/xdbf/xdbf.h" #include "xenia/kernel/xbdm/xbdm_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" #include "xenia/memory.h" @@ -741,15 +741,20 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, config::LoadGameConfig(title_id); uint32_t resource_data = 0; uint32_t resource_size = 0; - if (XSUCCEEDED(module->GetSection(title_id.c_str(), &resource_data, - &resource_size))) { - kernel::util::XdbfGameData db( - module->memory()->TranslateVirtual(resource_data), resource_size); - if (db.is_valid()) { - game_title_ = db.title(); - auto icon_block = db.icon(); + + if (XSUCCEEDED( + module->GetSection(title_id, &resource_data, &resource_size))) { + kernel::xam::xdbf::SpaFile spa; + if (spa.Read(module->memory()->TranslateVirtual(resource_data), + resource_size)) { + // Set title SPA and get title name/icon + kernel_state_->user_profile()->SetTitleSpaData(spa); + game_title_ = spa.GetTitleName(); + auto icon_block = spa.GetIcon(); + if (icon_block) { - display_window_->SetIcon(icon_block.buffer, icon_block.size); + display_window_->SetIcon(icon_block->data.data(), + icon_block->data.size()); } } } diff --git a/src/xenia/kernel/util/crypto_utils.cc b/src/xenia/kernel/util/crypto_utils.cc new file mode 100644 index 000000000..9c08441cf --- /dev/null +++ b/src/xenia/kernel/util/crypto_utils.cc @@ -0,0 +1,130 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ +#include + +#include "xenia/kernel/util/crypto_utils.h" +#include "xenia/xbox.h" + +#include "third_party/crypto/TinySHA1.hpp" + +namespace xe { +namespace kernel { +namespace util { + +uint8_t xekey_0x19[] = {0xE1, 0xBC, 0x15, 0x9C, 0x73, 0xB1, 0xEA, 0xE9, + 0xAB, 0x31, 0x70, 0xF3, 0xAD, 0x47, 0xEB, 0xF3}; + +uint8_t xekey_0x19_devkit[] = {0xDA, 0xB6, 0x9A, 0xD9, 0x8E, 0x28, 0x76, 0x4F, + 0x97, 0x7E, 0xE2, 0x48, 0x7E, 0x4F, 0x3F, 0x68}; + +const uint8_t* GetXeKey(uint32_t idx, bool devkit) { + if (idx != 0x19) { + return nullptr; + } + + return devkit ? xekey_0x19_devkit : xekey_0x19; +} + +void HmacSha(const uint8_t* key, uint32_t key_size_in, const uint8_t* inp_1, + uint32_t inp_1_size, const uint8_t* inp_2, uint32_t inp_2_size, + const uint8_t* inp_3, uint32_t inp_3_size, uint8_t* out, + uint32_t out_size) { + uint32_t key_size = key_size_in; + sha1::SHA1 sha; + uint8_t kpad_i[0x40]; + uint8_t kpad_o[0x40]; + uint8_t tmp_key[0x40]; + std::memset(kpad_i, 0x36, 0x40); + std::memset(kpad_o, 0x5C, 0x40); + + // Setup HMAC key + // If > block size, use its hash + if (key_size > 0x40) { + sha1::SHA1 sha_key; + sha_key.processBytes(key, key_size); + sha_key.finalize((uint8_t*)tmp_key); + + key_size = 0x14u; + } else { + std::memcpy(tmp_key, key, key_size); + } + + for (uint32_t i = 0; i < key_size; i++) { + kpad_i[i] = tmp_key[i] ^ 0x36; + kpad_o[i] = tmp_key[i] ^ 0x5C; + } + + // Inner + sha.processBytes(kpad_i, 0x40); + + if (inp_1_size) { + sha.processBytes(inp_1, inp_1_size); + } + + if (inp_2_size) { + sha.processBytes(inp_2, inp_2_size); + } + + if (inp_3_size) { + sha.processBytes(inp_3, inp_3_size); + } + + uint8_t digest[0x14]; + sha.finalize(digest); + sha.reset(); + + // Outer + sha.processBytes(kpad_o, 0x40); + sha.processBytes(digest, 0x14); + sha.finalize(digest); + + std::memcpy(out, digest, std::min((uint32_t)out_size, 0x14u)); +} + +void RC4(const uint8_t* key, uint32_t key_size_in, const uint8_t* data, + uint32_t data_size, uint8_t* out, uint32_t out_size) { + uint8_t tmp_key[0x10]; + uint32_t sbox_size; + uint8_t sbox[0x100]; + uint32_t i; + uint32_t j; + + // Setup RC4 session... + std::memcpy(tmp_key, key, 0x10); + i = j = 0; + sbox_size = 0x100; + for (uint32_t x = 0; x < sbox_size; x++) { + sbox[x] = (uint8_t)x; + } + + uint32_t idx = 0; + for (uint32_t x = 0; x < sbox_size; x++) { + idx = (idx + sbox[x] + key[x % 0x10]) % sbox_size; + uint8_t temp = sbox[idx]; + sbox[idx] = sbox[x]; + sbox[x] = temp; + } + + // Crypt data + for (uint32_t idx = 0; idx < data_size; idx++) { + i = (i + 1) % sbox_size; + j = (j + sbox[i]) % sbox_size; + uint8_t temp = sbox[i]; + sbox[i] = sbox[j]; + sbox[j] = temp; + + uint8_t a = data[idx]; + uint8_t b = sbox[(sbox[i] + sbox[j]) % sbox_size]; + out[idx] = (uint8_t)(a ^ b); + } +} + +} // namespace util +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/util/crypto_utils.h b/src/xenia/kernel/util/crypto_utils.h new file mode 100644 index 000000000..456b7cff7 --- /dev/null +++ b/src/xenia/kernel/util/crypto_utils.h @@ -0,0 +1,28 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/xbox.h" + +namespace xe { +namespace kernel { +namespace util { + +const uint8_t* GetXeKey(uint32_t idx, bool devkit = false); + +void HmacSha(const uint8_t* key, uint32_t key_size_in, const uint8_t* inp_1, + uint32_t inp_1_size, const uint8_t* inp_2, uint32_t inp_2_size, + const uint8_t* inp_3, uint32_t inp_3_size, uint8_t* out, + uint32_t out_size); + +void RC4(const uint8_t* key, uint32_t key_size_in, const uint8_t* data, + uint32_t data_size, uint8_t* out, uint32_t out_size); + +} // namespace util +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/util/xdbf_utils.cc b/src/xenia/kernel/util/xdbf_utils.cc deleted file mode 100644 index e79b15449..000000000 --- a/src/xenia/kernel/util/xdbf_utils.cc +++ /dev/null @@ -1,107 +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" - -namespace xe { -namespace kernel { -namespace util { - -constexpr uint32_t kXdbfMagicXdbf = 'XDBF'; -constexpr uint32_t kXdbfMagicXstc = 'XSTC'; -constexpr uint32_t kXdbfMagicXstr = 'XSTR'; - -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(ptr); - ptr += sizeof(XbdfHeader); - if (header_->magic != kXdbfMagicXdbf) { - data_ = nullptr; - return; - } - - entries_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfEntry) * header_->entry_count; - - files_ = reinterpret_cast(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(section) && entry.id == id) { - XdbfBlock block; - block.buffer = content_offset_ + entry.offset; - block.size = entry.size; - return block; - } - } - return {0}; -} - -std::string XdbfWrapper::GetStringTableEntry(XdbfLocale locale, - uint16_t string_id) const { - auto language_block = - GetEntry(XdbfSection::kStringTable, static_cast(locale)); - if (!language_block) { - return ""; - } - - auto xstr_head = - reinterpret_cast(language_block.buffer); - assert_true(xstr_head->magic == kXdbfMagicXstr); - assert_true(xstr_head->version == 1); - - const uint8_t* ptr = language_block.buffer + sizeof(XdbfXstrHeader); - for (uint16_t i = 0; i < xstr_head->string_count; ++i) { - auto entry = reinterpret_cast(ptr); - ptr += sizeof(XdbfStringTableEntry); - if (entry->id == string_id) { - return std::string(reinterpret_cast(ptr), - entry->string_length); - } - ptr += entry->string_length; - } - return ""; -} - -constexpr uint64_t kXdbfIdTitle = 0x8000; -constexpr uint64_t kXdbfIdXstc = 0x58535443; - -XdbfBlock XdbfGameData::icon() const { - return GetEntry(XdbfSection::kImage, kXdbfIdTitle); -} - -XdbfLocale XdbfGameData::default_language() const { - auto block = GetEntry(XdbfSection::kMetadata, kXdbfIdXstc); - if (!block.buffer) { - return XdbfLocale::kEnglish; - } - auto xstc = reinterpret_cast(block.buffer); - assert_true(xstc->magic == kXdbfMagicXstc); - return static_cast(static_cast(xstc->default_language)); -} - -std::string XdbfGameData::title() const { - return GetStringTableEntry(default_language(), kXdbfIdTitle); -} - -} // namespace util -} // namespace kernel -} // namespace xe diff --git a/src/xenia/kernel/util/xdbf_utils.h b/src/xenia/kernel/util/xdbf_utils.h deleted file mode 100644 index 83ba12e27..000000000 --- a/src/xenia/kernel/util/xdbf_utils.h +++ /dev/null @@ -1,146 +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 -#include - -#include "xenia/base/memory.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, -}; - -// Found by dumping the kSectionStringTable sections of various games: -enum class XdbfLocale : uint32_t { - kUnknown = 0, - kEnglish = 1, - kJapanese = 2, - kGerman = 3, - kFrench = 4, - kSpanish = 5, - kItalian = 6, - kKorean = 7, - kChinese = 8, -}; - -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(XdbfLocale locale, uint16_t string_id) const; - - protected: -#pragma pack(push, 1) - struct XbdfHeader { - xe::be magic; - xe::be version; - xe::be entry_count; - xe::be entry_used; - xe::be free_count; - xe::be free_used; - }; - static_assert_size(XbdfHeader, 24); - - struct XbdfEntry { - xe::be section; - xe::be id; - xe::be offset; - xe::be size; - }; - static_assert_size(XbdfEntry, 18); - - struct XbdfFileLoc { - xe::be offset; - xe::be size; - }; - static_assert_size(XbdfFileLoc, 8); - - struct XdbfXstc { - xe::be magic; - xe::be version; - xe::be size; - xe::be default_language; - }; - static_assert_size(XdbfXstc, 16); - - struct XdbfXstrHeader { - xe::be magic; - xe::be version; - xe::be size; - xe::be string_count; - }; - static_assert_size(XdbfXstrHeader, 14); - - struct XdbfStringTableEntry { - xe::be id; - xe::be string_length; - }; - static_assert_size(XdbfStringTableEntry, 4); -#pragma pack(pop) - - 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) {} - - // The game icon image, if found. - XdbfBlock icon() const; - - // The game's default language. - XdbfLocale default_language() const; - - // The game's title in its default language. - std::string title() const; -}; - -} // namespace util -} // namespace kernel -} // namespace xe - -#endif // XENIA_KERNEL_UTIL_XDBF_UTILS_H_ diff --git a/src/xenia/kernel/xam/apps/xgi_app.cc b/src/xenia/kernel/xam/apps/xgi_app.cc index 1e994a0be..f1308738c 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -17,6 +17,11 @@ namespace kernel { namespace xam { namespace apps { +struct X_XUSER_ACHIEVEMENT { + xe::be user_idx; + xe::be achievement_id; +}; + XgiApp::XgiApp(KernelState* kernel_state) : App(kernel_state, 0xFB) {} // http://mb.mirage.org/bugzilla/xliveless/main.c @@ -55,6 +60,32 @@ X_RESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, uint32_t achievements_ptr = xe::load_and_swap(buffer + 4); XELOGD("XGIUserWriteAchievements({:08X}, {:08X})", achievement_count, achievements_ptr); + + auto* game_gpd = kernel_state_->user_profile()->GetTitleGpd(); + if (!game_gpd) { + XELOGE("XGIUserWriteAchievements failed, no game GPD set?"); + return X_ERROR_SUCCESS; + } + + bool modified = false; + auto* achievement = + (X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr); + xdbf::Achievement ach; + for (uint32_t i = 0; i < achievement_count; i++, achievement++) { + if (game_gpd->GetAchievement(achievement->achievement_id, &ach)) { + if (!ach.IsUnlocked()) { + XELOGI("Achievement Unlocked! {} ({} gamerscore) - {}", + to_utf8(ach.label), ach.gamerscore, to_utf8(ach.description)); + ach.Unlock(false); + game_gpd->UpdateAchievement(ach); + modified = true; + } + } + } + if (modified) { + kernel_state_->user_profile()->UpdateTitleGpd(); + } + return X_ERROR_SUCCESS; } case 0x000B0010: { diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 4982a02c6..0d1fa3035 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -7,25 +7,112 @@ ****************************************************************************** */ -#include "xenia/kernel/xam/user_profile.h" - #include #include "third_party/fmt/include/fmt/format.h" +#include "xenia/base/clock.h" +#include "xenia/base/cvar.h" +#include "xenia/base/filesystem.h" +#include "xenia/base/logging.h" +#include "xenia/base/mapped_memory.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/crypto_utils.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/kernel/xam/user_profile.h" namespace xe { namespace kernel { namespace xam { -UserProfile::UserProfile() { +DEFINE_path(profile_directory, "Content\\Profile\\", + "The directory to store profile data inside", "Storage"); + +constexpr uint32_t kDashboardID = 0xFFFE07D1; + +std::u16string X_XAMACCOUNTINFO::GetGamertagString() const { return gamertag; } + +bool UserProfile::DecryptAccountFile(const uint8_t* data, + X_XAMACCOUNTINFO* output, bool devkit) { + const uint8_t* key = util::GetXeKey(0x19, devkit); + if (!key) { + return false; // this shouldn't happen... + } + + // Generate RC4 key from data hash + uint8_t rc4_key[0x14]; + util::HmacSha(key, 0x10, data, 0x10, 0, 0, 0, 0, rc4_key, 0x14); + + uint8_t dec_data[sizeof(X_XAMACCOUNTINFO) + 8]; + + // Decrypt data + util::RC4(rc4_key, 0x10, data + 0x10, sizeof(dec_data), dec_data, + sizeof(dec_data)); + + // Verify decrypted data against hash + uint8_t data_hash[0x14]; + util::HmacSha(key, 0x10, dec_data, sizeof(dec_data), 0, 0, 0, 0, data_hash, + 0x14); + + if (std::memcmp(data, data_hash, 0x10) == 0) { + // Copy account data to output + std::memcpy(output, dec_data + 8, sizeof(X_XAMACCOUNTINFO)); + + // Swap gamertag endian + xe::copy_and_swap(output->gamertag, output->gamertag, 0x10); + return true; + } + + return false; +} + +void UserProfile::EncryptAccountFile(const X_XAMACCOUNTINFO* input, + uint8_t* output, bool devkit) { + const uint8_t* key = util::GetXeKey(0x19, devkit); + if (!key) { + return; // this shouldn't happen... + } + + X_XAMACCOUNTINFO* output_acct = (X_XAMACCOUNTINFO*)(output + 0x18); + std::memcpy(output_acct, input, sizeof(X_XAMACCOUNTINFO)); + + // Swap gamertag endian + xe::copy_and_swap(output_acct->gamertag, output_acct->gamertag, + 0x10); + + // Set confounder, should be random but meh + std::memset(output + 0x10, 0xFD, 8); + + // Encrypted data = xam account info + 8 byte confounder + uint32_t enc_data_size = sizeof(X_XAMACCOUNTINFO) + 8; + + // Set data hash + uint8_t data_hash[0x14]; + util::HmacSha(key, 0x10, output + 0x10, enc_data_size, 0, 0, 0, 0, data_hash, + 0x14); + + std::memcpy(output, data_hash, 0x10); + + // Generate RC4 key from data hash + uint8_t rc4_key[0x14]; + util::HmacSha(key, 0x10, data_hash, 0x10, 0, 0, 0, 0, rc4_key, 0x14); + + // Encrypt data + util::RC4(rc4_key, 0x10, output + 0x10, enc_data_size, output + 0x10, + enc_data_size); +} + +UserProfile::UserProfile() : dash_gpd_(kDashboardID) { // NeoGeo Battle Coliseum 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." - xuid_ = 0xB13EBABEBABEBABE; - name_ = "User"; + account_.xuid_online = 0xB13EBABEBABEBABE; + + // TODO: How to deal with this nullterm? + auto default_user_name = u"XeniaUser\0\0"; + + std::memcpy(account_.gamertag, default_user_name, + (std::u16string(default_user_name).size() * 2) + 2); // 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 @@ -91,6 +178,345 @@ UserProfile::UserProfile() { AddSetting(std::make_unique(0x63E83FFE)); // XPROFILE_TITLE_SPECIFIC3 AddSetting(std::make_unique(0x63E83FFD)); + + // Try loading profile GPD files... + LoadProfile(); +} + +void UserProfile::LoadProfile() { + auto mmap_ = MappedMemory::Open(cvars::profile_directory / L"Account", + MappedMemory::Mode::kRead); + if (mmap_) { + XELOGI("Loading Account file from path {}Account", + cvars::profile_directory.generic_u8string()); + + X_XAMACCOUNTINFO tmp_acct; + bool success = DecryptAccountFile(mmap_->data(), &tmp_acct); + if (!success) { + success = DecryptAccountFile(mmap_->data(), &tmp_acct, true); + } + + if (!success) { + XELOGW("Failed to decrypt Account file data"); + } else { + std::memcpy(&account_, &tmp_acct, sizeof(X_XAMACCOUNTINFO)); + XELOGI("Loaded Account \"{}\" successfully!", name()); + } + + mmap_->Close(); + } + + XELOGI("Loading profile GPDs from path {}", + cvars::profile_directory.generic_u8string()); + + mmap_ = MappedMemory::Open(cvars::profile_directory / L"FFFE07D1.gpd", + MappedMemory::Mode::kRead); + if (!mmap_) { + XELOGW( + "Failed to open dash GPD (FFFE07D1.gpd) for reading, 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"%X.gpd", title.title_id); + mmap_ = MappedMemory::Open(cvars::profile_directory / fname, + MappedMemory::Mode::kRead); + if (!mmap_) { + XELOGE("Failed to open GPD for title {:08X} ({})!", title.title_id, + to_utf8(title.title_name)); + continue; + } + + xdbf::GpdFile title_gpd(title.title_id); + bool result = title_gpd.Read(mmap_->data(), mmap_->size()); + mmap_->Close(); + + if (!result) { + XELOGE("Failed to read GPD for title {:08X} ({})!", title.title_id, + to_utf8(title.title_name)); + continue; + } + + title_gpds_[title.title_id] = title_gpd; + } + + XELOGI("Loaded {:08X} profile GPDs", title_gpds_.size() + 1); +} + +xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) { + uint32_t spa_title = spa_data.GetTitleId(); + + std::vector spa_achievements; + // TODO: let user choose locale? + spa_data.GetAchievements(spa_data.GetDefaultLocale(), &spa_achievements); + + xdbf::TitlePlayed title_info; + + auto gpd = title_gpds_.find(spa_title); + if (gpd != title_gpds_.end()) { + auto& title_gpd = (*gpd).second; + + XELOGI("Loaded existing GPD for title {:08X}", spa_title); + + bool always_update_title = false; + if (!dash_gpd_.GetTitle(spa_title, &title_info)) { + assert_always(); + XELOGE( + "GPD exists but is missing XbdfTitlePlayed entry? (this shouldn't be " + "happening!)"); + // Try to work around it... + title_info.title_name = to_utf16(spa_data.GetTitleName()); + title_info.title_id = spa_title; + title_info.achievements_possible = 0; + title_info.achievements_earned = 0; + title_info.gamerscore_total = 0; + title_info.gamerscore_earned = 0; + always_update_title = true; + } + title_info.last_played = Clock::QueryHostSystemTime(); + + // Check SPA for any achievements current GPD might be missing + // (maybe added in TUs etc?) + bool ach_updated = false; + for (auto ach : spa_achievements) { + bool ach_exists = title_gpd.GetAchievement(ach.id, nullptr); + if (ach_exists && !always_update_title) { + continue; + } + + // Achievement doesn't exist in current title info, lets add it + title_info.achievements_possible++; + title_info.gamerscore_total += ach.gamerscore; + + // If it doesn't exist in GPD, add it to that too + if (!ach_exists) { + XELOGD( + "Adding new achievement {} ({}) from SPA (wasn't inside existing " + "GPD)", + ach.id, to_utf8(ach.label)); + + ach_updated = true; + title_gpd.UpdateAchievement(ach); + } + } + + // Update dash with new title_info + dash_gpd_.UpdateTitle(title_info); + + // Only write game GPD if achievements were updated + if (ach_updated) { + UpdateGpd(spa_title, title_gpd); + } + UpdateGpd(kDashboardID, dash_gpd_); + } else { + // GPD not found... have to create it! + XELOGI("Creating new GPD for title {:08X}", spa_title); + + title_info.title_name = to_utf16(spa_data.GetTitleName()); + title_info.title_id = spa_title; + title_info.last_played = Clock::QueryHostSystemTime(); + + // Copy cheevos from SPA -> GPD + xdbf::GpdFile title_gpd(spa_title); + 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(xdbf::SpaSection::kImage), ach.image_id); + if (image_entry) { + title_gpd.UpdateEntry(*image_entry); + } + } + + // Try adding title image & name + auto* title_image = + spa_data.GetEntry(static_cast(xdbf::SpaSection::kImage), + static_cast(xdbf::SpaID::Title)); + if (title_image) { + title_gpd.UpdateEntry(*title_image); + } + + auto title_name = to_utf16(spa_data.GetTitleName()); + if (title_name.length()) { + xdbf::Entry title_name_ent; + title_name_ent.info.section = + static_cast(xdbf::GpdSection::kString); + title_name_ent.info.id = static_cast(xdbf::SpaID::Title); + title_name_ent.data.resize((title_name.length() + 1) * 2); + xe::copy_and_swap((char16_t*)title_name_ent.data.data(), + title_name.data(), title_name.length()); + title_gpd.UpdateEntry(title_name_ent); + } + + 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_); + } + + curr_gpd_ = &title_gpds_[spa_title]; + curr_title_id_ = spa_title; + + // Print achievement list to log, ATM there's no other way for users to see + // achievement status... + std::vector achievements; + if (curr_gpd_->GetAchievements(&achievements)) { + XELOGI("Achievement list:"); + + for (auto ach : achievements) { + // TODO: use ach.unachieved_desc for locked achievements? + // depends on XdbfAchievementFlags::kShowUnachieved afaik + XELOGI("{} - {} - {} - {} GS - {}", ach.id, to_utf8(ach.label), + to_utf8(ach.description), ach.gamerscore, + ach.IsUnlocked() ? "unlocked" : "locked"); + } + + XELOGI("Unlocked achievements: {}/{}, gamerscore: {}/{}\r\n", + title_info.achievements_earned, title_info.achievements_possible, + title_info.gamerscore_earned, title_info.gamerscore_total); + } + + return curr_gpd_; +} + +xdbf::GpdFile* UserProfile::GetTitleGpd(uint32_t title_id) { + if (title_id == -1) { + return curr_gpd_; + } + + auto gpd = title_gpds_.find(title_id); + if (gpd == title_gpds_.end()) { + return nullptr; + } + + return &(*gpd).second; +} + +void UserProfile::GetTitles(std::vector& titles) { + for (auto title : title_gpds_) { + titles.push_back(&title.second); + } +} + +bool UserProfile::UpdateTitleGpd(uint32_t title_id) { + if (title_id == -1) { + if (!curr_gpd_ || curr_title_id_ == -1) { + return false; + } + title_id = curr_title_id_; + } + + bool result = UpdateGpd(title_id, *curr_gpd_); + if (!result) { + XELOGE("UpdateTitleGpd failed on title {:08X}!", title_id); + } else { + XELOGD("Updated title {:08X} GPD successfully!", title_id); + } + return result; +} + +bool UserProfile::UpdateAllGpds() { + for (const auto& pair : title_gpds_) { + auto gpd = pair.second; + bool result = UpdateGpd(pair.first, gpd); + if (!result) { + XELOGE("UpdateGpdFiles failed on title {:08X}!", pair.first); + continue; + } + } + + // 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, xdbf::GpdFile& gpd_data) { + size_t gpd_length = 0; + if (!gpd_data.Write(nullptr, &gpd_length)) { + XELOGE("Failed to get GPD size for title {:08X}!", title_id); + return false; + } + + if (!std::filesystem::exists(cvars::profile_directory)) { + std::filesystem::create_directories(cvars::profile_directory); + } + + wchar_t fname[256]; + _swprintf(fname, L"%X.gpd", title_id); + + filesystem::CreateFile(cvars::profile_directory / fname); + auto mmap_ = + MappedMemory::Open(cvars::profile_directory / fname, + MappedMemory::Mode::kReadWrite, 0, gpd_length); + if (!mmap_) { + XELOGE("Failed to open {:08X}.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 {:08X}!", title_id); + ret_val = false; + } else { + // Check if we need to update dashboard data... + if (title_id != kDashboardID) { + xdbf::TitlePlayed title_info; + if (dash_gpd_.GetTitle(title_id, &title_info)) { + std::vector gpd_achievements; + 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; + } + } + + // Only update dash GPD if something has changed + 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_); + + // TODO: update gamerscore/achievements earned/titles played settings + // in dashboard 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 a5c03f254..2ed0911c4 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -15,12 +15,118 @@ #include #include +#include "xenia/kernel/xam/xdbf/xdbf.h" #include "xenia/xbox.h" namespace xe { namespace kernel { namespace xam { +// from https://github.com/xemio/testdev/blob/master/xkelib/xam/_xamext.h +#pragma pack(push, 4) +struct X_XAMACCOUNTINFO { + enum AccountReservedFlags { + kPasswordProtected = 0x10000000, + kLiveEnabled = 0x20000000, + kRecovering = 0x40000000, + kVersionMask = 0x000000FF + }; + + enum AccountUserFlags { + kPaymentInstrumentCreditCard = 1, + + kCountryMask = 0xFF00, + kSubscriptionTierMask = 0xF00000, + kLanguageMask = 0x3E000000, + + kParentalControlEnabled = 0x1000000, + }; + + enum AccountSubscriptionTier { + kSubscriptionTierSilver = 3, + kSubscriptionTierGold = 6, + kSubscriptionTierFamilyGold = 9 + }; + + // already exists inside xdbf.h?? + enum AccountLanguage { + kNoLanguage, + kEnglish, + kJapanese, + kGerman, + kFrench, + kSpanish, + kItalian, + kKorean, + kTChinese, + kPortuguese, + kSChinese, + kPolish, + kRussian, + kNorwegian = 15 + }; + + enum AccountLiveFlags { kAcctRequiresManagement = 1 }; + + xe::be reserved_flags; + xe::be live_flags; + char16_t gamertag[0x10]; + xe::be xuid_online; // 09.... + xe::be cached_user_flags; + xe::be network_id; + char passcode[4]; + char online_domain[0x14]; + char online_kerberos_realm[0x18]; + char online_key[0x10]; + char passport_membername[0x72]; + char passport_password[0x20]; + char owner_passport_membername[0x72]; + + bool IsPasscodeEnabled() { + return (bool)(reserved_flags & AccountReservedFlags::kPasswordProtected); + } + + bool IsLiveEnabled() { + return (bool)(reserved_flags & AccountReservedFlags::kLiveEnabled); + } + + bool IsRecovering() { + return (bool)(reserved_flags & AccountReservedFlags::kRecovering); + } + + bool IsPaymentInstrumentCreditCard() { + return (bool)(cached_user_flags & + AccountUserFlags::kPaymentInstrumentCreditCard); + } + + bool IsParentalControlled() { + return (bool)(cached_user_flags & + AccountUserFlags::kParentalControlEnabled); + } + + bool IsXUIDOffline() { return ((xuid_online >> 60) & 0xF) == 0xE; } + bool IsXUIDOnline() { return ((xuid_online >> 48) & 0xFFFF) == 0x9; } + bool IsXUIDValid() { return IsXUIDOffline() != IsXUIDOnline(); } + bool IsTeamXUID() { + return (xuid_online & 0xFF00000000000140) == 0xFE00000000000100; + } + + uint32_t GetCountry() { return (cached_user_flags & kCountryMask) >> 8; } + + AccountSubscriptionTier GetSubscriptionTier() { + return (AccountSubscriptionTier)( + (cached_user_flags & kSubscriptionTierMask) >> 20); + } + + AccountLanguage GetLanguage() { + return (AccountLanguage)((cached_user_flags & kLanguageMask) >> 25); + } + + std::u16string GetGamertagString() const; +}; +static_assert_size(X_XAMACCOUNTINFO, 0x17C); +#pragma pack(pop) + class UserProfile { public: struct Setting { @@ -197,23 +303,44 @@ class UserProfile { } }; + static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output, + bool devkit = false); + + static void EncryptAccountFile(const X_XAMACCOUNTINFO* input, uint8_t* output, + bool devkit = false); + UserProfile(); - uint64_t xuid() const { return xuid_; } - std::string name() const { return name_; } + uint64_t xuid() const { return account_.xuid_online; } + std::string name() const { return to_utf8(account_.GetGamertagString()); } uint32_t signin_state() const { return 1; } void AddSetting(std::unique_ptr setting); Setting* GetSetting(uint32_t setting_id); + xdbf::GpdFile* SetTitleSpaData(const xdbf::SpaFile& spa_data); + xdbf::GpdFile* GetTitleGpd(uint32_t title_id = -1); + + void GetTitles(std::vector& titles); + + bool UpdateTitleGpd(uint32_t title_id = -1); + bool UpdateAllGpds(); + private: - uint64_t xuid_; - std::string name_; + void LoadProfile(); + bool UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data); + + X_XAMACCOUNTINFO account_; std::vector> setting_list_; std::unordered_map settings_; void LoadSetting(UserProfile::Setting*); void SaveSetting(UserProfile::Setting*); + + std::unordered_map title_gpds_; + xdbf::GpdFile dash_gpd_; + xdbf::GpdFile* curr_gpd_ = nullptr; + uint32_t curr_title_id_ = -1; }; } // namespace xam diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index a499aa13a..50169f499 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -21,6 +21,83 @@ namespace xe { namespace kernel { namespace xam { +struct X_PROFILEENUMRESULT { + xe::be xuid_offline; // E0..... + X_XAMACCOUNTINFO account; + xe::be device_id; +}; +static_assert_size(X_PROFILEENUMRESULT, 0x188); + +dword_result_t XamProfileCreateEnumerator(dword_t device_id, + lpdword_t handle_out) { + assert_not_null(handle_out); + + auto e = + new XStaticEnumerator(kernel_state(), 1, sizeof(X_PROFILEENUMRESULT)); + + e->Initialize(); + + const auto& user_profile = kernel_state()->user_profile(); + + X_PROFILEENUMRESULT* profile = (X_PROFILEENUMRESULT*)e->AppendItem(); + memset(profile, 0, sizeof(X_PROFILEENUMRESULT)); + profile->xuid_offline = user_profile->xuid(); + profile->device_id = 0xF00D0000; + + auto tag = to_utf16(user_profile->name()); + xe::copy_and_swap(profile->account.gamertag, tag.c_str(), + tag.length()); + profile->account.xuid_online = user_profile->xuid(); + + *handle_out = e->handle(); + return X_ERROR_SUCCESS; +} +DECLARE_XAM_EXPORT1(XamProfileCreateEnumerator, kUserProfiles, kImplemented); + +dword_result_t XamProfileEnumerate(dword_t handle, dword_t flags, + lpvoid_t buffer, + pointer_t overlapped) { + assert_true(flags == 0); + + auto e = kernel_state()->object_table()->LookupObject(handle); + if (!e) { + if (overlapped) { + kernel_state()->CompleteOverlappedImmediateEx( + overlapped, X_ERROR_INVALID_HANDLE, X_ERROR_INVALID_HANDLE, 0); + return X_ERROR_IO_PENDING; + } else { + return X_ERROR_INVALID_HANDLE; + } + } + + buffer.Zero(sizeof(X_PROFILEENUMRESULT)); + + X_RESULT result; + + if (e->current_item() >= e->item_count()) { + result = X_ERROR_NO_MORE_FILES; + } else { + auto item_buffer = buffer.as(); + if (!e->WriteItem(item_buffer)) { + result = X_ERROR_NO_MORE_FILES; + } else { + result = X_ERROR_SUCCESS; + } + } + + // Return X_ERROR_NO_MORE_FILES in HRESULT form. + X_HRESULT extended_result = result != 0 ? X_HRESULT_FROM_WIN32(result) : 0; + if (overlapped) { + kernel_state()->CompleteOverlappedImmediateEx( + overlapped, result, extended_result, result == X_ERROR_SUCCESS ? 1 : 0); + return X_ERROR_IO_PENDING; + } else { + assert_always(); + return X_ERROR_INVALID_PARAMETER; + } +} +DECLARE_XAM_EXPORT1(XamProfileEnumerate, kUserProfiles, kImplemented); + X_HRESULT_result_t XamUserGetXUID(dword_t user_index, dword_t type_mask, lpqword_t xuid_ptr) { if (!xuid_ptr) { @@ -507,6 +584,20 @@ dword_result_t XamShowSigninUI(dword_t unk, dword_t unk_mask) { } DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kStub); +#pragma pack(push, 1) +struct X_XACHIEVEMENT_DETAILS { + xe::be id; + xe::be label_ptr; + xe::be description_ptr; + xe::be unachieved_ptr; + xe::be image_id; + xe::be gamerscore; + xe::be unlock_time; + xe::be flags; +}; +static_assert_size(X_XACHIEVEMENT_DETAILS, 36); +#pragma pack(pop) + dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id, dword_t user_index, dword_t xuid, dword_t flags, @@ -514,14 +605,60 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id, lpdword_t buffer_size_ptr, lpdword_t handle_ptr) { if (buffer_size_ptr) { - *buffer_size_ptr = 500 * count; + *buffer_size_ptr = sizeof(X_XACHIEVEMENT_DETAILS) * count; } - auto e = new XStaticEnumerator(kernel_state(), count, 500); + auto e = new XStaticEnumerator(kernel_state(), count, + sizeof(X_XACHIEVEMENT_DETAILS)); e->Initialize(); *handle_ptr = e->handle(); + // Copy achievements into the enumerator if game GPD is loaded + auto* game_gpd = kernel_state()->user_profile()->GetTitleGpd(title_id); + if (!game_gpd) { + XELOGE( + "XamUserCreateAchievementEnumerator failed to find GPD for title %X!", + title_id); + return X_ERROR_SUCCESS; + } + + static uint32_t placeholder = 0; + + if (!placeholder) { + wchar_t* placeholder_val = L""; + + placeholder = kernel_memory()->SystemHeapAlloc( + ((uint32_t)wcslen(placeholder_val) + 1) * 2); + auto* place_addr = kernel_memory()->TranslateVirtual(placeholder); + + memset(place_addr, 0, (wcslen(placeholder_val) + 1) * 2); + xe::copy_and_swap(place_addr, placeholder_val, wcslen(placeholder_val)); + } + + std::vector achievements; + game_gpd->GetAchievements(&achievements); + + for (auto ach : achievements) { + auto* details = (X_XACHIEVEMENT_DETAILS*)e->AppendItem(); + details->id = ach.id; + details->image_id = ach.image_id; + details->gamerscore = ach.gamerscore; + details->unlock_time = ach.unlock_time; + details->flags = ach.flags; + + // TODO: these, allocating guest mem for them every CreateEnum call would be + // very bad... + + // maybe we could alloc these in guest when the title GPD is first loaded? + details->label_ptr = placeholder; + details->description_ptr = placeholder; + details->unachieved_ptr = placeholder; + } + + XELOGD("XamUserCreateAchievementEnumerator: added %d items to enumerator", + e->item_count()); + return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XamUserCreateAchievementEnumerator, kUserProfiles, diff --git a/src/xenia/kernel/xam/xdbf/xdbf.cc b/src/xenia/kernel/xam/xdbf/xdbf.cc new file mode 100644 index 000000000..98f91b6d1 --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/xdbf.cc @@ -0,0 +1,431 @@ +/** + ****************************************************************************** + * 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/xam/xdbf/xdbf.h" +#include "xenia/base/string.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace xdbf { + +constexpr uint32_t kXdbfMagicXdbf = 'XDBF'; + +bool XdbfFile::Read(const uint8_t* data, size_t data_size) { + if (!data || data_size <= sizeof(X_XDBF_HEADER)) { + return false; + } + + auto* ptr = data; + memcpy(&header_, ptr, sizeof(X_XDBF_HEADER)); + if (header_.magic != kXdbfMagicXdbf) { + return false; + } + + ptr += sizeof(X_XDBF_HEADER); + + auto* free_ptr = (const X_XDBF_FILELOC*)(ptr + (sizeof(X_XDBF_ENTRY) * + header_.entry_count)); + auto* data_ptr = + (uint8_t*)free_ptr + (sizeof(X_XDBF_FILELOC) * header_.free_count); + + for (uint32_t i = 0; i < header_.entry_used; i++) { + Entry entry; + memcpy(&entry.info, ptr, sizeof(X_XDBF_ENTRY)); + entry.data.resize(entry.info.size); + memcpy(entry.data.data(), data_ptr + entry.info.offset, entry.info.size); + entries_.push_back(entry); + + ptr += sizeof(X_XDBF_ENTRY); + } + + for (uint32_t i = 0; i < header_.free_used; i++) { + free_entries_.push_back(*free_ptr); + free_ptr++; + } + + return true; +} + +bool XdbfFile::Write(uint8_t* data, size_t* data_size) { + *data_size = 0; + + *data_size += sizeof(X_XDBF_HEADER); + *data_size += entries_.size() * sizeof(X_XDBF_ENTRY); + *data_size += 1 * sizeof(X_XDBF_FILELOC); + + size_t entries_size = 0; + for (auto ent : entries_) { + entries_size += ent.data.size(); + } + + *data_size += entries_size; + + if (!data) { + return true; + } + + header_.entry_count = header_.entry_used = (uint32_t)entries_.size(); + header_.free_count = header_.free_used = 1; + + auto* ptr = data; + memcpy(ptr, &header_, sizeof(X_XDBF_HEADER)); + ptr += sizeof(X_XDBF_HEADER); + + auto* free_ptr = + (X_XDBF_FILELOC*)(ptr + (sizeof(X_XDBF_ENTRY) * header_.entry_count)); + auto* data_start = + (uint8_t*)free_ptr + (sizeof(X_XDBF_FILELOC) * header_.free_count); + + auto* data_ptr = data_start; + for (auto ent : entries_) { + ent.info.offset = (uint32_t)(data_ptr - data_start); + ent.info.size = (uint32_t)ent.data.size(); + memcpy(ptr, &ent.info, sizeof(X_XDBF_ENTRY)); + + memcpy(data_ptr, ent.data.data(), ent.data.size()); + data_ptr += ent.data.size(); + ptr += sizeof(X_XDBF_ENTRY); + } + + free_entries_.clear(); + X_XDBF_FILELOC free_ent; + free_ent.offset = (uint32_t)*data_size - sizeof(X_XDBF_HEADER) - + (sizeof(X_XDBF_ENTRY) * header_.entry_count) - + (sizeof(X_XDBF_FILELOC) * header_.free_count); + + free_ent.size = 0 - free_ent.offset; + free_entries_.push_back(free_ent); + + for (auto ent : free_entries_) { + memcpy(free_ptr, &ent, sizeof(X_XDBF_FILELOC)); + free_ptr++; + } + + return true; +} + +Entry* XdbfFile::GetEntry(uint16_t section, uint64_t id) const { + for (size_t i = 0; i < entries_.size(); i++) { + auto* entry = (Entry*)&entries_[i]; + if (entry->info.section != section || entry->info.id != id) { + continue; + } + + return entry; + } + + return nullptr; +} + +bool XdbfFile::UpdateEntry(Entry entry) { + for (size_t i = 0; i < entries_.size(); i++) { + auto* ent = (Entry*)&entries_[i]; + if (ent->info.section != entry.info.section || + ent->info.id != entry.info.id) { + continue; + } + + ent->data = entry.data; + ent->info.size = (uint32_t)entry.data.size(); + return true; + } + + Entry new_entry; + new_entry.info.section = entry.info.section; + new_entry.info.id = entry.info.id; + new_entry.info.size = (uint32_t)entry.data.size(); + new_entry.data = entry.data; + + entries_.push_back(new_entry); + return true; +} + +std::string GetStringTableEntry_(const uint8_t* table_start, uint16_t string_id, + uint16_t count) { + auto* ptr = table_start; + for (uint16_t i = 0; i < count; ++i) { + auto entry = reinterpret_cast(ptr); + ptr += sizeof(XdbfStringTableEntry); + if (entry->id == string_id) { + return std::string(reinterpret_cast(ptr), entry->string_length); + } + ptr += entry->string_length; + } + return ""; +} + +std::string SpaFile::GetStringTableEntry(Locale locale, + uint16_t string_id) const { + auto xstr_table = GetEntry(static_cast(SpaSection::kStringTable), + static_cast(locale)); + if (!xstr_table) { + return ""; + } + + auto xstr_head = + reinterpret_cast(xstr_table->data.data()); + assert_true(xstr_head->magic == static_cast(SpaID::Xstr)); + assert_true(xstr_head->version == 1); + + const uint8_t* ptr = xstr_table->data.data() + sizeof(X_XDBF_TABLE_HEADER); + + return GetStringTableEntry_(ptr, string_id, xstr_head->count); +} + +uint32_t SpaFile::GetAchievements( + Locale locale, std::vector* achievements) const { + auto xach_table = GetEntry(static_cast(SpaSection::kMetadata), + static_cast(SpaID::Xach)); + if (!xach_table) { + return 0; + } + + auto xach_head = + reinterpret_cast(xach_table->data.data()); + assert_true(xach_head->magic == static_cast(SpaID::Xach)); + assert_true(xach_head->version == 1); + + auto xstr_table = GetEntry(static_cast(SpaSection::kStringTable), + static_cast(locale)); + if (!xstr_table) { + return 0; + } + + auto xstr_head = + reinterpret_cast(xstr_table->data.data()); + assert_true(xstr_head->magic == static_cast(SpaID::Xstr)); + assert_true(xstr_head->version == 1); + + const uint8_t* xstr_ptr = + xstr_table->data.data() + sizeof(X_XDBF_TABLE_HEADER); + + if (achievements) { + auto* ach_data = + reinterpret_cast(xach_head + 1); + for (uint32_t i = 0; i < xach_head->count; i++) { + Achievement ach; + ach.id = ach_data->id; + ach.image_id = ach_data->image_id; + ach.gamerscore = ach_data->gamerscore; + ach.flags = ach_data->flags; + + ach.label = to_utf16(GetStringTableEntry_(xstr_ptr, ach_data->label_id, xstr_head->count)); + ach.description = to_utf16(GetStringTableEntry_( + xstr_ptr, ach_data->description_id, xstr_head->count)); + ach.unachieved_desc = to_utf16(GetStringTableEntry_( + xstr_ptr, ach_data->unachieved_id, xstr_head->count)); + + achievements->push_back(ach); + ach_data++; + } + } + + return xach_head->count; +} + +Entry* SpaFile::GetIcon() const { + return GetEntry(static_cast(SpaSection::kImage), + static_cast(SpaID::Title)); +} + +Locale SpaFile::GetDefaultLocale() const { + auto block = GetEntry(static_cast(SpaSection::kMetadata), + static_cast(SpaID::Xstc)); + if (!block) { + return Locale::kEnglish; + } + + auto xstc = reinterpret_cast(block->data.data()); + assert_true(xstc->magic == static_cast(SpaID::Xstc)); + + return static_cast(static_cast(xstc->default_language)); +} + +std::string SpaFile::GetTitleName() const { + return GetStringTableEntry(GetDefaultLocale(), + static_cast(SpaID::Title)); +} + +uint32_t SpaFile::GetTitleId() const { + auto block = GetEntry(static_cast(SpaSection::kMetadata), + static_cast(SpaID::Xthd)); + if (!block) { + return -1; + } + + auto xthd = reinterpret_cast(block->data.data()); + assert_true(xthd->magic == static_cast(SpaID::Xthd)); + + return xthd->title_id; +} + +bool GpdFile::GetAchievement(uint16_t id, Achievement* dest) { + for (size_t i = 0; i < entries_.size(); i++) { + auto* entry = (Entry*)&entries_[i]; + if (entry->info.section != + static_cast(GpdSection::kAchievement) || + entry->info.id != id) { + continue; + } + + auto* ach_data = + reinterpret_cast(entry->data.data()); + + if (dest) { + dest->ReadGPD(ach_data); + } + return true; + } + + return false; +} + +uint32_t GpdFile::GetAchievements( + std::vector* achievements) const { + uint32_t ach_count = 0; + + for (size_t i = 0; i < entries_.size(); i++) { + auto* entry = (Entry*)&entries_[i]; + if (entry->info.section != + static_cast(GpdSection::kAchievement)) { + continue; + } + if (entry->info.id == 0x100000000 || entry->info.id == 0x200000000) { + continue; // achievement sync data, ignore it + } + + ach_count++; + + if (achievements) { + auto* ach_data = + reinterpret_cast(entry->data.data()); + + Achievement ach; + ach.ReadGPD(ach_data); + + achievements->push_back(ach); + } + } + + return ach_count; +} + +bool GpdFile::GetTitle(uint32_t title_id, TitlePlayed* dest) { + for (size_t i = 0; i < entries_.size(); i++) { + auto* entry = (Entry*)&entries_[i]; + if (entry->info.section != static_cast(GpdSection::kTitle) || + entry->info.id != title_id) { + continue; + } + + auto* title_data = + reinterpret_cast(entry->data.data()); + + dest->ReadGPD(title_data); + + return true; + } + + return false; +} + +uint32_t GpdFile::GetTitles(std::vector* titles) const { + uint32_t title_count = 0; + + for (size_t i = 0; i < entries_.size(); i++) { + auto* entry = (Entry*)&entries_[i]; + if (entry->info.section != static_cast(GpdSection::kTitle)) { + continue; + } + if (entry->info.id == 0x100000000 || entry->info.id == 0x200000000) { + continue; // achievement sync data, ignore it + } + + title_count++; + + if (titles) { + auto* title_data = + reinterpret_cast(entry->data.data()); + + TitlePlayed title; + title.ReadGPD(title_data); + titles->push_back(title); + } + } + + return title_count; +} + +bool GpdFile::UpdateAchievement(Achievement ach) { + Entry ent; + ent.info.section = static_cast(GpdSection::kAchievement); + ent.info.id = ach.id; + + // calculate entry size... + size_t label_len = (ach.label.length() * 2) + 2; + size_t desc_len = (ach.description.length() * 2) + 2; + size_t unach_len = (ach.unachieved_desc.length() * 2) + 2; + + size_t est_size = sizeof(X_XDBF_GPD_ACHIEVEMENT); + est_size += label_len; + est_size += desc_len; + est_size += unach_len; + + ent.data.resize(est_size); + memset(ent.data.data(), 0, est_size); + + // convert Achievement to GPD achievement + auto* ach_data = reinterpret_cast(ent.data.data()); + ach_data->id = ach.id; + ach_data->image_id = ach.image_id; + ach_data->gamerscore = ach.gamerscore; + ach_data->flags = ach.flags; + ach_data->unlock_time = ach.unlock_time; + + auto* label_ptr = reinterpret_cast(ent.data.data() + + sizeof(X_XDBF_GPD_ACHIEVEMENT)); + auto* desc_ptr = label_ptr + label_len; + auto* unach_ptr = desc_ptr + desc_len; + + xe::copy_and_swap((char16_t*)label_ptr, ach.label.c_str(), + ach.label.size()); + xe::copy_and_swap((char16_t*)desc_ptr, ach.description.c_str(), + ach.description.size()); + xe::copy_and_swap((char16_t*)unach_ptr, ach.unachieved_desc.c_str(), + ach.unachieved_desc.size()); + + return UpdateEntry(ent); +} + +bool GpdFile::UpdateTitle(TitlePlayed title) { + Entry ent; + ent.info.section = static_cast(GpdSection::kTitle); + ent.info.id = title.title_id; + + // calculate entry size... + size_t name_len = (title.title_name.length() * 2) + 2; + + size_t est_size = sizeof(X_XDBF_GPD_TITLEPLAYED); + est_size += name_len; + + ent.data.resize(est_size); + memset(ent.data.data(), 0, est_size); + + // convert XdbfTitlePlayed to GPD title + auto* title_data = reinterpret_cast(ent.data.data()); + title.WriteGPD(title_data); + + return UpdateEntry(ent); +} + +} // namespace xdbf +} // namespace xam +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/xam/xdbf/xdbf.h b/src/xenia/kernel/xam/xdbf/xdbf.h new file mode 100644 index 000000000..c15f6ff6c --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/xdbf.h @@ -0,0 +1,269 @@ +/** + ****************************************************************************** + * 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_XAM_XDBF_XDBF_H_ +#define XENIA_KERNEL_XAM_XDBF_XDBF_H_ + +#include +#include + +#include "xenia/base/clock.h" +#include "xenia/base/memory.h" + +#include "xenia/kernel/xam/xdbf/xdbf_xbox.h" + +namespace xe { +namespace kernel { +namespace xam { +namespace xdbf { + +// 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 SpaID : uint64_t { + Xach = 'XACH', + Xstr = 'XSTR', + Xstc = 'XSTC', + Xthd = 'XTHD', + Title = 0x8000, +}; + +enum class SpaSection : uint16_t { + kMetadata = 0x1, + kImage = 0x2, + kStringTable = 0x3, +}; + +enum class GpdSection : uint16_t { + kAchievement = 0x1, + kImage = 0x2, + kSetting = 0x3, + kTitle = 0x4, + kString = 0x5, + kSecurity = 0x6 +}; + +// Found by dumping the kSectionStringTable sections of various games: +enum class Locale : uint32_t { + kUnknown = 0, + kEnglish = 1, + kJapanese = 2, + kGerman = 3, + kFrench = 4, + kSpanish = 5, + kItalian = 6, + kKorean = 7, + kChinese = 8, +}; + +inline std::u16string ReadNullTermString(const char16_t* ptr) { + std::u16string retval; + wchar_t data = xe::byte_swap(*ptr); + while (data != 0) { + retval += data; + ptr++; + data = xe::byte_swap(*ptr); + } + return retval; +} + +struct TitlePlayed { + uint32_t title_id = 0; + std::u16string title_name; + uint32_t achievements_possible = 0; + uint32_t achievements_earned = 0; + uint32_t gamerscore_total = 0; + uint32_t gamerscore_earned = 0; + uint16_t reserved_achievement_count = 0; + uint16_t all_avatar_awards = 0; + uint16_t male_avatar_awards = 0; + uint16_t female_avatar_awards = 0; + uint32_t reserved_flags = 0; + uint64_t last_played = 0; + + void ReadGPD(const X_XDBF_GPD_TITLEPLAYED* src) { + title_id = src->title_id; + achievements_possible = src->achievements_possible; + achievements_earned = src->achievements_earned; + gamerscore_total = src->gamerscore_total; + gamerscore_earned = src->gamerscore_earned; + reserved_achievement_count = src->reserved_achievement_count; + all_avatar_awards = src->all_avatar_awards; + male_avatar_awards = src->male_avatar_awards; + female_avatar_awards = src->female_avatar_awards; + reserved_flags = src->reserved_flags; + last_played = src->last_played; + + auto* txt_ptr = reinterpret_cast(src + 1); + title_name = ReadNullTermString((const char16_t*)txt_ptr); + } + + void WriteGPD(X_XDBF_GPD_TITLEPLAYED* dest) { + dest->title_id = title_id; + dest->achievements_possible = achievements_possible; + dest->achievements_earned = achievements_earned; + dest->gamerscore_total = gamerscore_total; + dest->gamerscore_earned = gamerscore_earned; + dest->reserved_achievement_count = reserved_achievement_count; + dest->all_avatar_awards = all_avatar_awards; + dest->male_avatar_awards = male_avatar_awards; + dest->female_avatar_awards = female_avatar_awards; + dest->reserved_flags = reserved_flags; + dest->last_played = last_played; + + auto* txt_ptr = reinterpret_cast(dest + 1); + xe::copy_and_swap((char16_t*)txt_ptr, (char16_t*)title_name.c_str(), + title_name.size()); + } +}; + +enum class AchievementType : uint32_t { + kCompletion = 1, + kLeveling = 2, + kUnlock = 3, + kEvent = 4, + kTournament = 5, + kCheckpoint = 6, + kOther = 7, +}; + +enum class AchievementFlags : uint32_t { + kTypeMask = 0x7, + kShowUnachieved = 0x8, + kAchievedOnline = 0x10000, + kAchieved = 0x20000 +}; + +struct Achievement { + uint16_t id = 0; + std::u16string label; + std::u16string description; + std::u16string unachieved_desc; + uint32_t image_id = 0; + uint32_t gamerscore = 0; + uint32_t flags = 0; + uint64_t unlock_time = 0; + + AchievementType GetType() { + return static_cast( + flags & static_cast(AchievementFlags::kTypeMask)); + } + + bool IsUnlocked() { + return flags & static_cast(AchievementFlags::kAchieved); + } + + bool IsUnlockedOnline() { + return flags & static_cast(AchievementFlags::kAchievedOnline); + } + + void Unlock(bool online = false) { + flags |= static_cast(AchievementFlags::kAchieved); + if (online) { + flags |= static_cast(AchievementFlags::kAchievedOnline); + } + + unlock_time = Clock::QueryHostSystemTime(); + } + + void Lock() { + flags = flags & ~(static_cast(AchievementFlags::kAchieved)); + flags = flags & ~(static_cast(AchievementFlags::kAchievedOnline)); + unlock_time = 0; + } + + void ReadGPD(const X_XDBF_GPD_ACHIEVEMENT* src) { + id = src->id; + image_id = src->image_id; + gamerscore = src->gamerscore; + flags = src->flags; + unlock_time = src->unlock_time; + + auto* txt_ptr = reinterpret_cast(src + 1); + + label = ReadNullTermString((const char16_t*)txt_ptr); + + txt_ptr += (label.length() * 2) + 2; + description = ReadNullTermString((const char16_t*)txt_ptr); + + txt_ptr += (description.length() * 2) + 2; + unachieved_desc = ReadNullTermString((const char16_t*)txt_ptr); + } +}; + +struct Entry { + X_XDBF_ENTRY info; + std::vector data; +}; + +// Parses/creates an XDBF (XboxDataBaseFormat) file +// http://www.free60.org/wiki/XDBF +class XdbfFile { + public: + XdbfFile() { + header_.magic = 'XDBF'; + header_.version = 1; + } + + bool Read(const uint8_t* data, size_t data_size); + bool Write(uint8_t* data, size_t* data_size); + + Entry* GetEntry(uint16_t section, uint64_t id) const; + + // Updates (or adds) an entry + bool UpdateEntry(Entry entry); + + protected: + X_XDBF_HEADER header_; + std::vector entries_; + std::vector free_entries_; +}; + +class SpaFile : public XdbfFile { + public: + std::string GetStringTableEntry(Locale locale, uint16_t string_id) const; + + uint32_t GetAchievements(Locale locale, + std::vector* achievements) const; + + Entry* GetIcon() const; + Locale GetDefaultLocale() const; + std::string GetTitleName() const; + uint32_t GetTitleId() const; +}; + +class GpdFile : public XdbfFile { + public: + GpdFile() : title_id_(-1) {} + GpdFile(uint32_t title_id) : title_id_(title_id) {} + + bool GetAchievement(uint16_t id, Achievement* dest); + uint32_t GetAchievements(std::vector* achievements) const; + + bool GetTitle(uint32_t title_id, TitlePlayed* title); + uint32_t GetTitles(std::vector* titles) const; + + // Updates (or adds) an achievement + bool UpdateAchievement(Achievement ach); + + // Updates (or adds) a title + bool UpdateTitle(TitlePlayed title); + + uint32_t GetTitleId() { return title_id_; } + + private: + uint32_t title_id_ = -1; +}; + +} // namespace xdbf +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_XDBF_H_ diff --git a/src/xenia/kernel/xam/xdbf/xdbf_xbox.h b/src/xenia/kernel/xam/xdbf/xdbf_xbox.h new file mode 100644 index 000000000..cff02d68d --- /dev/null +++ b/src/xenia/kernel/xam/xdbf/xdbf_xbox.h @@ -0,0 +1,137 @@ +/** + ****************************************************************************** + * 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_XAM_XDBF_XDBF_XBOX_H_ +#define XENIA_KERNEL_XAM_XDBF_XDBF_XBOX_H_ + +namespace xe { +namespace kernel { +namespace xam { +namespace xdbf { + +/* Native XDBF structs used by 360 are in this file */ + +struct XdbfStringTableEntry { + xe::be id; + xe::be string_length; +}; +static_assert_size(XdbfStringTableEntry, 4); + +#pragma pack(push, 1) +struct X_XDBF_HEADER { + xe::be magic; + xe::be version; + xe::be entry_count; + xe::be entry_used; + xe::be free_count; + xe::be free_used; +}; +static_assert_size(X_XDBF_HEADER, 24); + +struct X_XDBF_ENTRY { + xe::be section; + xe::be id; + xe::be offset; + xe::be size; +}; +static_assert_size(X_XDBF_ENTRY, 18); + +struct X_XDBF_FILELOC { + xe::be offset; + xe::be size; +}; +static_assert_size(X_XDBF_FILELOC, 8); + +struct X_XDBF_XSTC_DATA { + xe::be magic; + xe::be version; + xe::be size; + xe::be default_language; +}; +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; + xe::be size; + xe::be count; +}; +static_assert_size(X_XDBF_TABLE_HEADER, 14); + +struct X_XDBF_SPA_ACHIEVEMENT { + xe::be id; + xe::be label_id; + xe::be description_id; + xe::be unachieved_id; + xe::be image_id; + xe::be gamerscore; + xe::be unkE; + xe::be flags; + xe::be unk14; + xe::be unk18; + xe::be unk1C; + xe::be unk20; +}; +static_assert_size(X_XDBF_SPA_ACHIEVEMENT, 0x24); + +struct X_XDBF_GPD_ACHIEVEMENT { + xe::be magic; + xe::be id; + xe::be image_id; + xe::be gamerscore; + xe::be flags; + xe::be unlock_time; + // wchar_t* title; + // wchar_t* description; + // wchar_t* unlocked_description; +}; + +// from https://github.com/xemio/testdev/blob/master/xkelib/xam/_xamext.h +struct X_XDBF_GPD_TITLEPLAYED { + xe::be title_id; + xe::be achievements_possible; + xe::be achievements_earned; + xe::be gamerscore_total; + xe::be gamerscore_earned; + xe::be reserved_achievement_count; + + // the following are meant to be split into possible/earned, 1 byte each + // but who cares + xe::be all_avatar_awards; + xe::be male_avatar_awards; + xe::be female_avatar_awards; + xe::be reserved_flags; + xe::be last_played; + // wchar_t* title_name; +}; +#pragma pack(pop) + +} // namespace xdbf +} // namespace xam +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_XAM_XDBF_XDBF_XBOX_H_ \ No newline at end of file diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc index 9a8e4a001..88a1907c4 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc @@ -9,6 +9,7 @@ #include "xenia/base/logging.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/crypto_utils.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/xbox.h" @@ -433,75 +434,17 @@ void XeCryptHmacSha(lpvoid_t key, dword_t key_size_in, lpvoid_t inp_1, dword_t inp_1_size, lpvoid_t inp_2, dword_t inp_2_size, lpvoid_t inp_3, dword_t inp_3_size, lpvoid_t out, dword_t out_size) { - uint32_t key_size = key_size_in; - sha1::SHA1 sha; - uint8_t kpad_i[0x40]; - uint8_t kpad_o[0x40]; - uint8_t tmp_key[0x40]; - std::memset(kpad_i, 0x36, 0x40); - std::memset(kpad_o, 0x5C, 0x40); - - // Setup HMAC key - // If > block size, use its hash - if (key_size > 0x40) { - sha1::SHA1 sha_key; - sha_key.processBytes(key, key_size); - sha_key.finalize((uint8_t*)tmp_key); - - key_size = 0x14u; - } else { - std::memcpy(tmp_key, key, key_size); - } - - for (uint32_t i = 0; i < key_size; i++) { - kpad_i[i] = tmp_key[i] ^ 0x36; - kpad_o[i] = tmp_key[i] ^ 0x5C; - } - - // Inner - sha.processBytes(kpad_i, 0x40); - - if (inp_1_size) { - sha.processBytes(inp_1, inp_1_size); - } - - if (inp_2_size) { - sha.processBytes(inp_2, inp_2_size); - } - - if (inp_3_size) { - sha.processBytes(inp_3, inp_3_size); - } - - uint8_t digest[0x14]; - sha.finalize(digest); - sha.reset(); - - // Outer - sha.processBytes(kpad_o, 0x40); - sha.processBytes(digest, 0x14); - sha.finalize(digest); - - std::memcpy(out, digest, std::min((uint32_t)out_size, 0x14u)); + util::HmacSha(key, key_size_in, inp_1, inp_1_size, inp_2, inp_2_size, inp_3, + inp_3_size, out, out_size); } DECLARE_XBOXKRNL_EXPORT1(XeCryptHmacSha, kNone, kImplemented); -// Keys -// TODO: Array of keys we need - -// Retail key 0x19 -static const uint8_t key19[] = {0xE1, 0xBC, 0x15, 0x9C, 0x73, 0xB1, 0xEA, 0xE9, - 0xAB, 0x31, 0x70, 0xF3, 0xAD, 0x47, 0xEB, 0xF3}; - dword_result_t XeKeysHmacSha(dword_t key_num, lpvoid_t inp_1, dword_t inp_1_size, lpvoid_t inp_2, dword_t inp_2_size, lpvoid_t inp_3, dword_t inp_3_size, lpvoid_t out, dword_t out_size) { - const uint8_t* key = nullptr; - if (key_num == 0x19) { - key = key19; - } + const uint8_t* key = util::GetXeKey(key_num); if (key) { XeCryptHmacSha((void*)key, 0x10, inp_1, inp_1_size, inp_2, inp_2_size,