diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 4f1c59da9..6c81dd828 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -13,6 +13,7 @@ #include #include +#include "xenia/app/profile_dialogs.h" #include "xenia/emulator.h" #include "xenia/gpu/command_processor.h" #include "xenia/ui/imgui_dialog.h" @@ -25,8 +26,6 @@ #include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" -#include "xenia/app/profile_dialogs.h" - namespace xe { namespace app { diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 10a0f0907..5f3afb646 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -43,6 +43,7 @@ #include "xenia/kernel/user_module.h" #include "xenia/kernel/util/gameinfo_utils.h" #include "xenia/kernel/util/xdbf_utils.h" +#include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/xam_module.h" #include "xenia/kernel/xbdm/xbdm_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" @@ -1517,6 +1518,17 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, if (!icon_block.empty()) { display_window_->SetIcon(icon_block.data(), icon_block.size()); } + + for (uint8_t slot = 0; slot < XUserMaxUserCount; slot++) { + auto user = + kernel_state_->xam_state()->profile_manager()->GetProfile(slot); + + if (user) { + kernel_state_->xam_state() + ->achievement_manager() + ->LoadTitleAchievements(user->xuid(), db); + } + } } } diff --git a/src/xenia/kernel/xam/achievement_manager.cc b/src/xenia/kernel/xam/achievement_manager.cc index 85d43c908..28abe2624 100644 --- a/src/xenia/kernel/xam/achievement_manager.cc +++ b/src/xenia/kernel/xam/achievement_manager.cc @@ -2,12 +2,12 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2023 Ben Vanik. All rights reserved. * + * Copyright 2024 Xenia Canary. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ -#include "achievement_manager.h" +#include "xenia/kernel/xam/achievement_manager.h" #include "xenia/emulator.h" #include "xenia/gpu/graphics_system.h" #include "xenia/kernel/kernel_state.h" @@ -17,52 +17,218 @@ DEFINE_bool(show_achievement_notification, false, "Show achievement notification on screen.", "UI"); +DEFINE_string(default_achievement_backend, "", + "Defines which achievement backend should be used as an default. " + "Possible options: [].", + "Achievements"); + DECLARE_int32(user_language); namespace xe { namespace kernel { namespace xam { -AchievementManager::AchievementManager() { unlocked_achievements.clear(); }; +GpdAchievementBackend::GpdAchievementBackend() {} +GpdAchievementBackend::~GpdAchievementBackend() {} -void AchievementManager::EarnAchievement(uint64_t xuid, uint32_t title_id, - uint32_t achievement_id) { - if (IsAchievementUnlocked(achievement_id)) { +void GpdAchievementBackend::EarnAchievement(const uint64_t xuid, + const uint32_t title_id, + const uint32_t achievement_id) { + const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { return; } - const Emulator* emulator = kernel_state()->emulator(); - ui::WindowedAppContext& app_context = - kernel_state()->emulator()->display_window()->app_context(); - ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + auto achievement = GetAchievementInfoInternal(xuid, title_id, achievement_id); + if (!achievement) { + return; + } + XELOGI("Player: {} Unlocked Achievement: {}", user->name(), + xe::to_utf8(xe::load_and_swap( + achievement->achievement_name.c_str()))); + + const uint64_t unlock_time = Clock::QueryHostSystemTime(); + achievement->flags = + achievement->flags | static_cast(AchievementFlags::kAchieved); + achievement->unlock_time.high_part = static_cast(unlock_time >> 32); + achievement->unlock_time.low_part = static_cast(unlock_time); + + SaveAchievementData(xuid, title_id, achievement_id); +} + +AchievementGpdStructure* GpdAchievementBackend::GetAchievementInfoInternal( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return nullptr; + } + + return user->GetAchievement(title_id, achievement_id); +} + +const AchievementGpdStructure* GpdAchievementBackend::GetAchievementInfo( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + return GetAchievementInfoInternal(xuid, title_id, achievement_id); +} + +bool GpdAchievementBackend::IsAchievementUnlocked( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + const auto achievement = + GetAchievementInfoInternal(xuid, title_id, achievement_id); + + return (achievement->flags & + static_cast(AchievementFlags::kAchieved)) != 0; +} + +const std::vector* +GpdAchievementBackend::GetTitleAchievements(const uint64_t xuid, + const uint32_t title_id) const { + const auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return {}; + } + + return user->GetTitleAchievements(title_id); +} + +bool GpdAchievementBackend::LoadAchievementsData( + const uint64_t xuid, const util::XdbfGameData title_data) { + auto user = kernel_state()->xam_state()->GetUserProfile(xuid); + if (!user) { + return false; + } + + // Question. Should loading for GPD for profile be directly done by profile or + // here? + if (!title_data.is_valid()) { + return false; + } + + const auto achievements = title_data.GetAchievements(); + if (achievements.empty()) { + return true; + } + + const auto title_id = title_data.GetTitleInformation().title_id; + + const XLanguage title_language = title_data.GetExistingLanguage( + static_cast(cvars::user_language)); + for (const auto& achievement : achievements) { + AchievementGpdStructure achievementData(title_language, title_data, + achievement); + user->achievements_[title_id].push_back(achievementData); + } + + // TODO(Gliniak): Here should be loader of GPD file for loaded title. That way + // we can load flags and unlock_time from specific user. + return true; +} + +bool GpdAchievementBackend::SaveAchievementData(const uint64_t xuid, + const uint32_t title_id, + const uint32_t achievement_id) { + return true; +} + +AchievementManager::AchievementManager() { + default_achievements_backend_ = std::make_unique(); + + // Add any optional backend here. +}; +void AchievementManager::EarnAchievement(const uint32_t user_index, + const uint32_t title_id, + const uint32_t achievement_id) const { + const auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return; + } + + EarnAchievement(user->xuid(), title_id, achievement_id); +}; + +void AchievementManager::EarnAchievement(const uint64_t xuid, + const uint32_t title_id, + const uint32_t achievement_id) const { + if (!DoesAchievementExist(achievement_id)) { + XELOGW( + "{}: Achievement with ID: {} for title: {:08X} doesn't exist in " + "database!", + __func__, achievement_id, title_id); + return; + } + // Always send request to unlock in 3PP backends. It's up to them to check if + // achievement was unlocked + for (auto& backend : achievement_backends_) { + backend->EarnAchievement(xuid, title_id, achievement_id); + } + + if (default_achievements_backend_->IsAchievementUnlocked(xuid, title_id, + achievement_id)) { + return; + } + + default_achievements_backend_->EarnAchievement(xuid, title_id, + achievement_id); + + if (!cvars::show_achievement_notification) { + return; + } + + const auto achievement = default_achievements_backend_->GetAchievementInfo( + xuid, title_id, achievement_id); + + if (!achievement) { + // Something went really wrong! + return; + } + ShowAchievementEarnedNotification(achievement); +} + +void AchievementManager::LoadTitleAchievements( + const uint64_t xuid, const util::XdbfGameData title_data) const { + default_achievements_backend_->LoadAchievementsData(xuid, title_data); +} + +const AchievementGpdStructure* AchievementManager::GetAchievementInfo( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const { + return default_achievements_backend_->GetAchievementInfo(xuid, title_id, + achievement_id); +} + +const std::vector* +AchievementManager::GetTitleAchievements(const uint64_t xuid, + const uint32_t title_id) const { + return default_achievements_backend_->GetTitleAchievements(xuid, title_id); +} + +bool AchievementManager::DoesAchievementExist( + const uint32_t achievement_id) const { const util::XdbfGameData title_xdbf = kernel_state()->title_xdbf(); const util::XdbfAchievementTableEntry achievement = title_xdbf.GetAchievement(achievement_id); if (!achievement.id) { - return; - } - - const XLanguage title_language = title_xdbf.GetExistingLanguage( - static_cast(cvars::user_language)); - - const std::string label = - title_xdbf.GetStringTableEntry(title_language, achievement.label_id); - const std::string desc = title_xdbf.GetStringTableEntry( - title_language, achievement.description_id); - - XELOGI("Achievement unlocked: {}", label); - - unlocked_achievements[achievement_id] = Clock::QueryHostSystemTime(); - // Even if we disable popup we still should store info that this - // achievement was earned. - if (!cvars::show_achievement_notification) { - return; + return false; } + return true; +} +void AchievementManager::ShowAchievementEarnedNotification( + const AchievementGpdStructure* achievement) const { const std::string description = - fmt::format("{}G - {}", achievement.gamerscore, label); + fmt::format("{}G - {}", achievement->gamerscore, + xe::to_utf8(xe::load_and_swap( + achievement->achievement_name.c_str()))); + + const Emulator* emulator = kernel_state()->emulator(); + ui::WindowedAppContext& app_context = + emulator->display_window()->app_context(); + ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); app_context.CallInUIThread([imgui_drawer, description]() { new xe::ui::AchievementNotificationWindow( @@ -71,21 +237,6 @@ void AchievementManager::EarnAchievement(uint64_t xuid, uint32_t title_id, }); } -bool AchievementManager::IsAchievementUnlocked(uint32_t achievement_id) { - auto itr = unlocked_achievements.find(achievement_id); - - return itr != unlocked_achievements.cend(); -} - -uint64_t AchievementManager::GetAchievementUnlockTime(uint32_t achievement_id) { - auto itr = unlocked_achievements.find(achievement_id); - if (itr == unlocked_achievements.cend()) { - return 0; - } - - return itr->second; -} - } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/achievement_manager.h b/src/xenia/kernel/xam/achievement_manager.h index e24114015..ca896db65 100644 --- a/src/xenia/kernel/xam/achievement_manager.h +++ b/src/xenia/kernel/xam/achievement_manager.h @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2023 Ben Vanik. All rights reserved. * + * Copyright 2024 Xenia Canary. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -14,16 +14,16 @@ #include #include +#include "xenia/kernel/util/xdbf_utils.h" #include "xenia/xbox.h" namespace xe { namespace kernel { namespace xam { -// TODO(gibbed): probably a FILETIME/LARGE_INTEGER, unknown currently struct X_ACHIEVEMENT_UNLOCK_TIME { - xe::be unk_0; - xe::be unk_4; + xe::be high_part; + xe::be low_part; }; struct X_ACHIEVEMENT_DETAILS { @@ -40,20 +40,160 @@ struct X_ACHIEVEMENT_DETAILS { }; static_assert_size(X_ACHIEVEMENT_DETAILS, 36); +// This is structure used inside GPD file. +// GPD is writeable XDBF. +// There are two info instances +// 1. In Dashboard ID which contains single GPD that contains info about any +// booted game (name, title_id, last boot time etc) +// 2. In specific Title ID directory GPD contains there structure below for +// every achievement. (unlocked or not) +struct AchievementGpdStructure { + AchievementGpdStructure(const XLanguage language, + const util::XdbfGameData xdbf, + const util::XdbfAchievementTableEntry& xdbf_entry) { + const std::string label = + xdbf.GetStringTableEntry(language, xdbf_entry.label_id); + const std::string desc = + xdbf.GetStringTableEntry(language, xdbf_entry.description_id); + const std::string locked_desc = + xdbf.GetStringTableEntry(language, xdbf_entry.unachieved_id); + + struct_size = 0x1C; + achievement_id = static_cast>(xdbf_entry.id); + image_id = xdbf_entry.image_id; + gamerscore = static_cast>(xdbf_entry.gamerscore); + flags = xdbf_entry.flags; + unlock_time = {}; + achievement_name = + xe::load_and_swap(xe::to_utf16(label).c_str()); + unlocked_description = + xe::load_and_swap(xe::to_utf16(desc).c_str()); + locked_description = + xe::load_and_swap(xe::to_utf16(locked_desc).c_str()); + } + + xe::be struct_size; + xe::be achievement_id; + xe::be image_id; + xe::be gamerscore; + xe::be flags; + X_ACHIEVEMENT_UNLOCK_TIME unlock_time; + std::u16string achievement_name; + std::u16string unlocked_description; + std::u16string locked_description; +}; + +enum class AchievementType : uint32_t { + kCompletion = 1, + kLeveling = 2, + kUnlock = 3, + kEvent = 4, + kTournament = 5, + kCheckpoint = 6, + kOther = 7, +}; + +enum class AchievementPlatform : uint32_t { + kX360 = 0x100000, + kPC = 0x200000, + kMobile = 0x300000, + kWebGames = 0x400000, +}; + +enum class AchievementFlags : uint32_t { + kTypeMask = 0x7, + kShowUnachieved = 0x8, + kAchievedOnline = 0x10000, + kAchieved = 0x20000, + kNotAchievable = 0x40000, + kWasNotAchievable = 0x80000, + kPlatformMask = 0x700000, + kColorizable = 0x1000000, // avatar awards only? +}; + +class AchievementBackendInterface { + public: + virtual ~AchievementBackendInterface() {}; + + virtual void EarnAchievement(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) = 0; + + virtual bool IsAchievementUnlocked(const uint64_t xuid, + const uint32_t title_id, + const uint32_t achievement_id) const = 0; + + virtual const AchievementGpdStructure* GetAchievementInfo( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const = 0; + virtual const std::vector* GetTitleAchievements( + const uint64_t xuid, const uint32_t title_id) const = 0; + virtual bool LoadAchievementsData(const uint64_t xuid, + const util::XdbfGameData title_data) = 0; + + private: + virtual bool SaveAchievementsData(const uint64_t xuid, + const uint32_t title_id) = 0; + virtual bool SaveAchievementData(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) = 0; +}; + +class GpdAchievementBackend : public AchievementBackendInterface { + public: + GpdAchievementBackend(); + ~GpdAchievementBackend(); + + void EarnAchievement(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) override; + bool IsAchievementUnlocked(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const override; + const AchievementGpdStructure* GetAchievementInfo( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const override; + const std::vector* GetTitleAchievements( + const uint64_t xuid, const uint32_t title_id) const override; + bool LoadAchievementsData(const uint64_t xuid, + const util::XdbfGameData title_data) override; + + private: + AchievementGpdStructure* GetAchievementInfoInternal( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const; + + bool SaveAchievementsData(const uint64_t xuid, + const uint32_t title_id) override { + return 0; + }; + bool SaveAchievementData(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) override; +}; + class AchievementManager { public: AchievementManager(); - void EarnAchievement(uint64_t xuid, uint32_t title_id, - uint32_t achievement_id); + void LoadTitleAchievements(const uint64_t xuid, + const util::XdbfGameData title_id) const; - bool IsAchievementUnlocked(uint32_t achievement_id); - uint64_t GetAchievementUnlockTime(uint32_t achievement_id); + void EarnAchievement(const uint32_t user_index, const uint32_t title_id, + const uint32_t achievement_id) const; + void EarnAchievement(const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const; + const AchievementGpdStructure* GetAchievementInfo( + const uint64_t xuid, const uint32_t title_id, + const uint32_t achievement_id) const; + const std::vector* GetTitleAchievements( + const uint64_t xuid, const uint32_t title_id) const; private: - std::map unlocked_achievements; - // void Load(); - // void Save(); + bool DoesAchievementExist(const uint32_t achievement_id) const; + void ShowAchievementEarnedNotification( + const AchievementGpdStructure* achievement) const; + + // This contains all backends with exception of default storage. + std::vector> + achievement_backends_; + + std::unique_ptr default_achievements_backend_; }; } // namespace xam diff --git a/src/xenia/kernel/xam/apps/xgi_app.cc b/src/xenia/kernel/xam/apps/xgi_app.cc index ab676b2ee..c2fa53014 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -102,7 +102,8 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, (X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr); for (uint32_t i = 0; i < achievement_count; i++, achievement++) { kernel_state_->achievement_manager()->EarnAchievement( - achievement->user_idx, 0, achievement->achievement_id); + achievement->user_idx, kernel_state_->title_id(), + achievement->achievement_id); } return X_E_SUCCESS; } diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 726ac0af0..dab6e3903 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -232,6 +232,31 @@ Property* UserProfile::GetProperty(const AttributeKey id) { return nullptr; } +AchievementGpdStructure* UserProfile::GetAchievement(const uint32_t title_id, + const uint32_t id) { + auto title_achievements = achievements_.find(title_id); + if (title_achievements == achievements_.end()) { + return nullptr; + } + + for (auto& entry : title_achievements->second) { + if (entry.achievement_id == id) { + return &entry; + } + } + return nullptr; +} + +std::vector* UserProfile::GetTitleAchievements( + const uint32_t title_id) { + auto title_achievements = achievements_.find(title_id); + if (title_achievements == achievements_.end()) { + return nullptr; + } + + return &title_achievements->second; +} + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index b4a5f4f68..8b16c8ecf 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -19,6 +19,7 @@ #include "xenia/base/byte_stream.h" #include "xenia/kernel/util/property.h" #include "xenia/kernel/util/xuserdata.h" +#include "xenia/kernel/xam/achievement_manager.h" #include "xenia/xbox.h" namespace xe { @@ -173,12 +174,21 @@ class UserProfile { std::map contexts_; + friend class GpdAchievementBackend; + + protected: + AchievementGpdStructure* GetAchievement(const uint32_t title_id, + const uint32_t id); + std::vector* GetTitleAchievements( + const uint32_t title_id); + private: uint64_t xuid_; X_XAMACCOUNTINFO account_info_; std::vector> setting_list_; std::unordered_map settings_; + std::map> achievements_; std::vector properties_; diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 5a4bdd70b..a35541140 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -609,32 +609,35 @@ dword_result_t XamUserCreateAchievementEnumerator_entry( return result; } + const auto user = kernel_state()->xam_state()->GetUserProfile(user_index); + if (!user) { + return X_ERROR_INVALID_PARAMETER; + } + + uint64_t requester_xuid = user->xuid(); + if (xuid) { + requester_xuid = xuid; + } + const util::XdbfGameData db = kernel_state()->title_xdbf(); + uint32_t title_id_ = title_id ? title_id : kernel_state()->title_id(); - if (db.is_valid()) { - const XLanguage language = - db.GetExistingLanguage(static_cast(cvars::user_language)); - const std::vector achievement_list = - db.GetAchievements(); - - for (const util::XdbfAchievementTableEntry& entry : achievement_list) { - auto is_unlocked = - kernel_state()->achievement_manager()->IsAchievementUnlocked( - entry.id); - auto unlock_time = - kernel_state()->achievement_manager()->GetAchievementUnlockTime( - entry.id); + const auto user_title_achievements = + kernel_state()->achievement_manager()->GetTitleAchievements( + requester_xuid, title_id_); + if (user_title_achievements) { + for (const auto& entry : *user_title_achievements) { auto item = XAchievementEnumerator::AchievementDetails{ - entry.id, - xe::to_utf16(db.GetStringTableEntry(language, entry.label_id)), - xe::to_utf16(db.GetStringTableEntry(language, entry.description_id)), - xe::to_utf16(db.GetStringTableEntry(language, entry.unachieved_id)), + entry.achievement_id, + xe::load_and_swap(entry.achievement_name.c_str()), + xe::load_and_swap(entry.unlocked_description.c_str()), + xe::load_and_swap(entry.locked_description.c_str()), entry.image_id, entry.gamerscore, - (uint32_t)(unlock_time << 31), - (uint32_t)unlock_time, - is_unlocked ? entry.flags | 0x20000 : entry.flags}; + entry.unlock_time.high_part, + entry.unlock_time.low_part, + entry.flags}; e->AppendItem(item); } diff --git a/src/xenia/kernel/xenumerator.cc b/src/xenia/kernel/xenumerator.cc index e2aacc816..19bf472a3 100644 --- a/src/xenia/kernel/xenumerator.cc +++ b/src/xenia/kernel/xenumerator.cc @@ -106,8 +106,8 @@ uint32_t XAchievementEnumerator::WriteItems(uint32_t buffer_ptr, !!(flags_ & 4) ? AppendString(string_buffer, item.unachieved) : 0; details[i].image_id = item.image_id; details[i].gamerscore = item.gamerscore; - details[i].unlock_time.unk_0 = item.unlock_time.unk_0; - details[i].unlock_time.unk_4 = item.unlock_time.unk_4; + details[i].unlock_time.high_part = item.unlock_time.high_part; + details[i].unlock_time.low_part = item.unlock_time.low_part; details[i].flags = item.flags; } diff --git a/src/xenia/kernel/xenumerator.h b/src/xenia/kernel/xenumerator.h index aa0a6ba2d..68f2fd9b4 100644 --- a/src/xenia/kernel/xenumerator.h +++ b/src/xenia/kernel/xenumerator.h @@ -124,8 +124,8 @@ class XAchievementEnumerator : public XEnumerator { uint32_t image_id; uint32_t gamerscore; struct { - uint32_t unk_0; - uint32_t unk_4; + uint32_t high_part; + uint32_t low_part; } unlock_time; uint32_t flags; };