diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index ef4005e28..172f935c6 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -669,6 +669,15 @@ bool EmulatorWindow::Initialize() { } main_menu->AddChild(std::move(hid_menu)); + // Game menu. + auto game_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Game"); + { + game_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "&Show achievements", "", + std::bind(&EmulatorWindow::ShowAchievements, this))); + } + main_menu->AddChild(std::move(game_menu)); + // Help menu. auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help"); { @@ -1755,6 +1764,12 @@ void EmulatorWindow::DisplayHotKeysConfig() { msg); } +void EmulatorWindow::ShowAchievements() { + if (!emulator_->is_title_open()) return; + + emulator_->kernel_state()->achievement_manager()->ShowAchievementsUI(0); +} + std::string EmulatorWindow::CanonicalizeFileExtension( const std::filesystem::path& path) { return xe::utf8::lower_ascii(xe::path_to_utf8(path.extension())); diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 1a0dc9564..7cc1092f6 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -235,6 +235,8 @@ class EmulatorWindow { bool IsUseNexusForGameBarEnabled(); void DisplayHotKeysConfig(); + void ShowAchievements(); + static std::string CanonicalizeFileExtension( const std::filesystem::path& path); diff --git a/src/xenia/kernel/achievement_manager.cc b/src/xenia/kernel/achievement_manager.cc index 743bf7f71..7198aadc6 100644 --- a/src/xenia/kernel/achievement_manager.cc +++ b/src/xenia/kernel/achievement_manager.cc @@ -12,6 +12,7 @@ #include "xenia/gpu/graphics_system.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_notification.h" DEFINE_bool(show_achievement_notification, false, @@ -181,5 +182,53 @@ void AchievementManager::Load(uint32_t user_index) { fclose(file); } +std::string AchievementManager::GetAchievementsUIText(uint32_t user_index) { + if (user_index >= 4) { + return ""; + } + + std::string ui_desc = ""; + + const util::XdbfGameData db = kernel_state()->title_xdbf(); + + 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) { + bool is_unlocked = IsAchievementUnlocked(user_index, entry.id); + + std::string mark = is_unlocked ? "[X]" : "[ ]"; + std::string label = db.GetStringTableEntry(language, entry.label_id); + uint16_t score = entry.gamerscore; + std::string description; + if (is_unlocked) { + description = db.GetStringTableEntry(language, entry.description_id); + } else { + description = db.GetStringTableEntry(language, entry.unachieved_id); + } + + ui_desc += fmt::format("{} {}\n {}\n {}G\n\n", mark, label, + description, score); + } + } + + return ui_desc; +} + +void AchievementManager::ShowAchievementsUI(uint32_t user_index) { + if (user_index >= 4) { + return; + } + + std::string ui_desc = GetAchievementsUIText(user_index); + + const Emulator* emulator = kernel_state()->emulator(); + ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + new ui::AchievementsDialog(imgui_drawer, ui_desc); +} + } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/achievement_manager.h b/src/xenia/kernel/achievement_manager.h index 70ba2bb3f..86a86c61f 100644 --- a/src/xenia/kernel/achievement_manager.h +++ b/src/xenia/kernel/achievement_manager.h @@ -50,6 +50,8 @@ class AchievementManager { uint32_t achievement_id); void Save(uint32_t user_index); void Load(uint32_t user_index); + std::string GetAchievementsUIText(uint32_t user_index); + void ShowAchievementsUI(uint32_t user_index); private: std::map unlocked_achievements[4]; diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index 3ebcae453..4fa8cfaf4 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -208,6 +208,49 @@ X_RESULT xeXamDispatchHeadlessEx( } } +template +X_RESULT xeXamDispatchDialogAsync(T* dialog, + std::function close_callback) { + // Broadcast XN_SYS_UI = true + kernel_state()->BroadcastNotification(0x9, true); + ++xam_dialogs_shown_; + + // Important to pass captured vars by value here since we return from this + // without waiting for the dialog to close so the original local vars will be + // destroyed. + // FIXME: Probably not the best idea to call Sleep in UI thread. + dialog->set_close_callback([dialog, close_callback]() { + close_callback(dialog); + + --xam_dialogs_shown_; + + xe::threading::Sleep(std::chrono::milliseconds(100)); + // Broadcast XN_SYS_UI = false + kernel_state()->BroadcastNotification(0x9, false); + }); + + return X_ERROR_SUCCESS; +} + +X_RESULT xeXamDispatchHeadlessAsync(std::function run_callback) { + // Broadcast XN_SYS_UI = true + kernel_state()->BroadcastNotification(0x9, true); + ++xam_dialogs_shown_; + + auto display_window = kernel_state()->emulator()->display_window(); + display_window->app_context().CallInUIThread([run_callback]() { + run_callback(); + + --xam_dialogs_shown_; + + xe::threading::Sleep(std::chrono::milliseconds(100)); + // Broadcast XN_SYS_UI = false + kernel_state()->BroadcastNotification(0x9, false); + }); + + return X_ERROR_SUCCESS; +} + dword_result_t XamIsUIActive_entry() { return xeXamIsUIActive(); } DECLARE_XAM_EXPORT2(XamIsUIActive, kUI, kImplemented, kHighFrequency); @@ -620,6 +663,31 @@ dword_result_t XamGetDashContext_entry(const ppc_context_t& ctx) { DECLARE_XAM_EXPORT1(XamGetDashContext, kNone, kImplemented); +dword_result_t XamShowAchievementsUI_entry(dword_t user_index) { + if (user_index >= 4) { + return X_ERROR_INVALID_PARAMETER; + } + + if (!kernel_state()->IsUserSignedIn(user_index)) { + return X_ERROR_NO_SUCH_USER; + } + + if (cvars::headless) { + return xeXamDispatchHeadlessAsync([]() {}); + } + + std::string ui_desc = + kernel_state()->achievement_manager()->GetAchievementsUIText(user_index); + + const Emulator* emulator = kernel_state()->emulator(); + ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + return xeXamDispatchDialogAsync( + new ui::AchievementsDialog(imgui_drawer, ui_desc), + [](ui::AchievementsDialog*) {}); +} + +DECLARE_XAM_EXPORT1(XamShowAchievementsUI, kUI, kSketchy); + } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/ui/imgui_dialog.cc b/src/xenia/ui/imgui_dialog.cc index 4e940e798..5c27956cd 100644 --- a/src/xenia/ui/imgui_dialog.cc +++ b/src/xenia/ui/imgui_dialog.cc @@ -87,5 +87,32 @@ ImGuiDialog* ImGuiDialog::ShowMessageBox(ImGuiDrawer* imgui_drawer, return new MessageBoxDialog(imgui_drawer, std::move(title), std::move(body)); } +void AchievementsDialog::OnDraw(ImGuiIO& io) { + if (!has_opened_) { + ImGui::OpenPopup("Achievements"); + has_opened_ = true; + } + if (ImGui::BeginPopupModal("Achievements", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + char* text = const_cast(body_.c_str()); + ImGui::InputTextMultiline( + "##body", text, body_.size(), ImVec2(600, 600), + ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_ReadOnly); + if (ImGui::Button("OK")) { + ImGui::CloseCurrentPopup(); + Close(); + } + ImGui::EndPopup(); + } else { + Close(); + } +} + +void AchievementsDialog::OnClose() { + if (close_callback_) { + close_callback_(); + } +} + } // namespace ui } // namespace xe diff --git a/src/xenia/ui/imgui_dialog.h b/src/xenia/ui/imgui_dialog.h index 454659bea..086598cc9 100644 --- a/src/xenia/ui/imgui_dialog.h +++ b/src/xenia/ui/imgui_dialog.h @@ -53,6 +53,25 @@ class ImGuiDialog { std::vector waiting_fences_; }; +class AchievementsDialog : public xe::ui::ImGuiDialog { + public: + AchievementsDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& body) + : xe::ui::ImGuiDialog(imgui_drawer), body_(body) {} + virtual ~AchievementsDialog() {} + + void set_close_callback(std::function close_callback) { + close_callback_ = close_callback; + } + + private: + void OnDraw(ImGuiIO& io) override; + void OnClose() override; + + bool has_opened_ = false; + std::string body_; + std::function close_callback_ = nullptr; +}; + } // namespace ui } // namespace xe