diff --git a/src/xenia/kernel/util/xdbf_utils.cc b/src/xenia/kernel/util/xdbf_utils.cc index e79b15449..691f7d53e 100644 --- a/src/xenia/kernel/util/xdbf_utils.cc +++ b/src/xenia/kernel/util/xdbf_utils.cc @@ -8,68 +8,139 @@ */ #include "xenia/kernel/util/xdbf_utils.h" +#include "xenia/base/string.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; +bool XdbfFile::Read(const uint8_t* data, size_t data_size) { + if (!data || data_size <= sizeof(X_XDBF_HEADER)) { + return false; } - const uint8_t* ptr = data_; - - header_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfHeader); - if (header_->magic != kXdbfMagicXdbf) { - data_ = nullptr; - return; + auto* ptr = data; + memcpy(&header, ptr, sizeof(X_XDBF_HEADER)); + if (header.magic != kXdbfMagicXdbf) { + return false; } - entries_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfEntry) * header_->entry_count; + ptr += sizeof(X_XDBF_HEADER); - files_ = reinterpret_cast(ptr); - ptr += sizeof(XbdfFileLoc) * header_->free_count; + 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); - content_offset_ = ptr; + for (uint32_t i = 0; i < header.entry_used; i++) { + XdbfEntry 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; } -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; +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 += free_entries.size() * 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 = (uint32_t)free_entries.size(); + + 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); + } + + for (auto ent : free_entries) { + memcpy(free_ptr, &ent, sizeof(X_XDBF_FILELOC)); + free_ptr++; + } + + return true; +} + +XdbfEntry* XdbfFile::GetEntry(uint16_t section, uint64_t id) const { + for (size_t i = 0; i < entries.size(); i++) { + auto* entry = (XdbfEntry*)&entries[i]; + if (entry->info.section != section || entry->info.id != id) { + continue; } + + return entry; } - return {0}; + + return nullptr; } -std::string XdbfWrapper::GetStringTableEntry(XdbfLocale locale, - uint16_t string_id) const { - auto language_block = - GetEntry(XdbfSection::kStringTable, static_cast(locale)); - if (!language_block) { - return ""; +bool XdbfFile::UpdateEntry(XdbfEntry entry) { + for (size_t i = 0; i < entries.size(); i++) { + auto* ent = (XdbfEntry*)&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; } - auto xstr_head = - reinterpret_cast(language_block.buffer); - assert_true(xstr_head->magic == kXdbfMagicXstr); - assert_true(xstr_head->version == 1); + XdbfEntry 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; - const uint8_t* ptr = language_block.buffer + sizeof(XdbfXstrHeader); - for (uint16_t i = 0; i < xstr_head->string_count; ++i) { + 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) { @@ -81,25 +152,219 @@ std::string XdbfWrapper::GetStringTableEntry(XdbfLocale locale, return ""; } -constexpr uint64_t kXdbfIdTitle = 0x8000; -constexpr uint64_t kXdbfIdXstc = 0x58535443; +std::string SpaFile::GetStringTableEntry(XdbfLocale locale, + uint16_t string_id) const { + auto xstr_table = + GetEntry(static_cast(XdbfSpaSection::kStringTable), + static_cast(locale)); + if (!xstr_table) { + return ""; + } -XdbfBlock XdbfGameData::icon() const { - return GetEntry(XdbfSection::kImage, kXdbfIdTitle); + auto xstr_head = + reinterpret_cast(xstr_table->data.data()); + assert_true(xstr_head->magic == static_cast(XdbfSpaID::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); } -XdbfLocale XdbfGameData::default_language() const { - auto block = GetEntry(XdbfSection::kMetadata, kXdbfIdXstc); - if (!block.buffer) { +uint32_t SpaFile::GetAchievements( + XdbfLocale locale, std::vector* achievements) const { + auto xach_table = GetEntry(static_cast(XdbfSpaSection::kMetadata), + static_cast(XdbfSpaID::Xach)); + if (!xach_table) { + return 0; + } + + auto xach_head = + reinterpret_cast(xach_table->data.data()); + assert_true(xach_head->magic == static_cast(XdbfSpaID::Xach)); + assert_true(xach_head->version == 1); + + auto xstr_table = + GetEntry(static_cast(XdbfSpaSection::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(XdbfSpaID::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++) { + XdbfAchievement 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 = xe::to_wstring( + GetStringTableEntry_(xstr_ptr, ach_data->label_id, xstr_head->count)); + + ach.description = xe::to_wstring(GetStringTableEntry_( + xstr_ptr, ach_data->description_id, xstr_head->count)); + + ach.unachieved_desc = xe::to_wstring(GetStringTableEntry_( + xstr_ptr, ach_data->unachieved_id, xstr_head->count)); + + achievements->push_back(ach); + ach_data++; + } + } + + return xach_head->count; +} + +XdbfEntry* SpaFile::GetIcon() const { + return GetEntry(static_cast(XdbfSpaSection::kImage), + static_cast(XdbfSpaID::Title)); +} + +XdbfLocale SpaFile::GetDefaultLocale() const { + auto block = GetEntry(static_cast(XdbfSpaSection::kMetadata), + static_cast(XdbfSpaID::Xstc)); + if (!block) { return XdbfLocale::kEnglish; } - auto xstc = reinterpret_cast(block.buffer); - assert_true(xstc->magic == kXdbfMagicXstc); + + auto xstc = reinterpret_cast(block->data.data()); + assert_true(xstc->magic == static_cast(XdbfSpaID::Xstc)); + return static_cast(static_cast(xstc->default_language)); } -std::string XdbfGameData::title() const { - return GetStringTableEntry(default_language(), kXdbfIdTitle); +std::string SpaFile::GetTitle() const { + return GetStringTableEntry(GetDefaultLocale(), + static_cast(XdbfSpaID::Title)); +} + +std::wstring ReadNullTermString(const wchar_t* ptr) { + std::wstring retval; + wchar_t data = xe::byte_swap(*ptr); + while (data != 0) { + retval += data; + ptr++; + data = xe::byte_swap(*ptr); + } + return retval; +} + +void ConvertGPDToXdbfAchievement(const X_XDBF_GPD_ACHIEVEMENT* src, + XdbfAchievement* dest) { + dest->id = src->id; + dest->image_id = src->image_id; + dest->gamerscore = src->gamerscore; + dest->flags = src->flags; + dest->unlock_time = src->unlock_time; + + auto* txt_ptr = reinterpret_cast(src + 1); + + dest->label = ReadNullTermString((const wchar_t*)txt_ptr); + + txt_ptr += (dest->label.length() * 2) + 2; + dest->description = ReadNullTermString((const wchar_t*)txt_ptr); + + txt_ptr += (dest->description.length() * 2) + 2; + dest->unachieved_desc = ReadNullTermString((const wchar_t*)txt_ptr); +} + +bool GpdFile::GetAchievement(uint16_t id, XdbfAchievement* dest) { + for (size_t i = 0; i < entries.size(); i++) { + auto* entry = (XdbfEntry*)&entries[i]; + if (entry->info.section != + static_cast(XdbfGpdSection::kAchievement) || + entry->info.id != id) { + continue; + } + + auto* ach_data = + reinterpret_cast(entry->data.data()); + + ConvertGPDToXdbfAchievement(ach_data, dest); + 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 = (XdbfEntry*)&entries[i]; + if (entry->info.section != + static_cast(XdbfGpdSection::kAchievement)) { + continue; + } + + ach_count++; + + if (achievements) { + auto* ach_data = + reinterpret_cast(entry->data.data()); + + XdbfAchievement ach; + ConvertGPDToXdbfAchievement(ach_data, &ach); + + achievements->push_back(ach); + } + } + + return ach_count; +} + +bool GpdFile::UpdateAchievement(XdbfAchievement ach) { + XdbfEntry ent; + ent.info.section = static_cast(XdbfGpdSection::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 XdbfAchievement 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((wchar_t*)label_ptr, ach.label.c_str(), + ach.label.size()); + xe::copy_and_swap((wchar_t*)desc_ptr, ach.description.c_str(), + ach.description.size()); + xe::copy_and_swap((wchar_t*)unach_ptr, ach.unachieved_desc.c_str(), + ach.unachieved_desc.size()); + + UpdateEntry(ent); + return true; } } // namespace util diff --git a/src/xenia/kernel/util/xdbf_utils.h b/src/xenia/kernel/util/xdbf_utils.h index 83ba12e27..2cb0d1d38 100644 --- a/src/xenia/kernel/util/xdbf_utils.h +++ b/src/xenia/kernel/util/xdbf_utils.h @@ -22,10 +22,26 @@ 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, +enum class XdbfSpaID : uint64_t { + Xach = 'XACH', + Xstr = 'XSTR', + Xstc = 'XSTC', + Title = 0x8000, +}; + +enum class XdbfSpaSection : uint16_t { + kMetadata = 0x1, + kImage = 0x2, + kStringTable = 0x3, +}; + +enum class XdbfGpdSection : uint16_t { + kAchievement = 0x1, + kImage = 0x2, + kSetting = 0x3, + kTitle = 0x4, + kString = 0x5, + kSecurity = 0x6 }; // Found by dumping the kSectionStringTable sections of various games: @@ -41,102 +57,186 @@ enum class XdbfLocale : uint32_t { kChinese = 8, }; -struct XdbfBlock { - const uint8_t* buffer; - size_t size; +struct XdbfStringTableEntry { + xe::be id; + xe::be string_length; +}; +static_assert_size(XdbfStringTableEntry, 4); - operator bool() const { return buffer != nullptr; } +#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_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; }; -// 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; +enum class XdbfAchievementType : uint32_t { + kCompletion = 1, + kLeveling = 2, + kUnlock = 3, + kEvent = 4, + kTournament = 5, + kCheckpoint = 6, + kOther = 7, }; -class XdbfGameData : public XdbfWrapper { +enum class XdbfAchievementFlags : uint32_t { + kTypeMask = 0x7, + kShowUnachieved = 0x8, + kAchievedOnline = 0x10000, + kAchieved = 0x20000 +}; + +struct XdbfAchievement { + uint16_t id = 0; + std::wstring label; + std::wstring description; + std::wstring unachieved_desc; + uint32_t image_id = 0; + uint32_t gamerscore = 0; + uint32_t flags = 0; + uint64_t unlock_time = 0; + + XdbfAchievementType GetType() { + return static_cast( + flags & static_cast(XdbfAchievementFlags::kTypeMask)); + } + + bool IsUnlocked() { + return flags & static_cast(XdbfAchievementFlags::kAchieved); + } + + bool IsUnlockedOnline() { + return flags & static_cast(XdbfAchievementFlags::kAchievedOnline); + } + + void Unlock(bool online = false) { + flags |= static_cast(XdbfAchievementFlags::kAchieved); + if (online) { + flags |= static_cast(XdbfAchievementFlags::kAchievedOnline); + } + // TODO: set unlock time? + } + + void Lock() { + flags = flags & ~(static_cast(XdbfAchievementFlags::kAchieved)); + flags = + flags & ~(static_cast(XdbfAchievementFlags::kAchievedOnline)); + unlock_time = 0; + } +}; + +struct XdbfEntry { + X_XDBF_ENTRY info; + std::vector data; +}; + +// Parses/creates an XDBF (XboxDataBaseFormat) file +// http://www.free60.org/wiki/XDBF +class XdbfFile { public: - XdbfGameData(const uint8_t* data, size_t data_size) - : XdbfWrapper(data, data_size) {} + XdbfFile() { + header.magic = 'XDBF'; + header.version = 1; + } - // The game icon image, if found. - XdbfBlock icon() const; + bool Read(const uint8_t* data, size_t data_size); + bool Write(uint8_t* data, size_t* data_size); - // The game's default language. - XdbfLocale default_language() const; + XdbfEntry* GetEntry(uint16_t section, uint64_t id) const; - // The game's title in its default language. - std::string title() const; + // Updates (or adds) an entry + bool UpdateEntry(XdbfEntry entry); + + protected: + X_XDBF_HEADER header; + std::vector entries; + std::vector free_entries; +}; + +class SpaFile : public XdbfFile { + public: + std::string GetStringTableEntry(XdbfLocale locale, uint16_t string_id) const; + + uint32_t GetAchievements(XdbfLocale locale, + std::vector* achievements) const; + + XdbfEntry* GetIcon() const; + XdbfLocale GetDefaultLocale() const; + std::string GetTitle() const; +}; + +class GpdFile : public XdbfFile { + public: + bool GetAchievement(uint16_t id, XdbfAchievement* dest); + uint32_t GetAchievements(std::vector* achievements) const; + + // Updates (or adds) an achievement + bool UpdateAchievement(XdbfAchievement ach); }; } // namespace util