achievements.cpp (122585B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR PolyForm-Strict-1.0.0) 3 4 // TODO: Don't poll when booting the game, e.g. Crash Warped freaks out. 5 6 #include "achievements.h" 7 #include "achievements_private.h" 8 #include "bios.h" 9 #include "bus.h" 10 #include "cpu_core.h" 11 #include "fullscreen_ui.h" 12 #include "host.h" 13 #include "system.h" 14 15 #include "scmversion/scmversion.h" 16 17 #include "common/assert.h" 18 #include "common/error.h" 19 #include "common/file_system.h" 20 #include "common/heap_array.h" 21 #include "common/log.h" 22 #include "common/md5_digest.h" 23 #include "common/path.h" 24 #include "common/scoped_guard.h" 25 #include "common/small_string.h" 26 #include "common/string_util.h" 27 #include "common/timer.h" 28 29 #include "util/cd_image.h" 30 #include "util/http_downloader.h" 31 #include "util/imgui_fullscreen.h" 32 #include "util/imgui_manager.h" 33 #include "util/platform_misc.h" 34 #include "util/state_wrapper.h" 35 36 #include "IconsEmoji.h" 37 #include "IconsFontAwesome5.h" 38 #include "IconsPromptFont.h" 39 #include "fmt/format.h" 40 #include "imgui.h" 41 #include "imgui_internal.h" 42 #include "rc_api_runtime.h" 43 #include "rc_client.h" 44 45 #include <algorithm> 46 #include <atomic> 47 #include <cstdarg> 48 #include <cstdlib> 49 #include <ctime> 50 #include <functional> 51 #include <string> 52 #include <unordered_set> 53 #include <vector> 54 55 Log_SetChannel(Achievements); 56 57 #ifdef ENABLE_RAINTEGRATION 58 // RA_Interface ends up including windows.h, with its silly macros. 59 #ifdef _WIN32 60 #include "common/windows_headers.h" 61 #endif 62 #include "RA_Interface.h" 63 #endif 64 namespace Achievements { 65 66 static constexpr const char* INFO_SOUND_NAME = "sounds/achievements/message.wav"; 67 static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav"; 68 static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav"; 69 static constexpr const char* ACHEIVEMENT_DETAILS_URL_TEMPLATE = "https://retroachievements.org/achievement/{}"; 70 static constexpr const char* CACHE_SUBDIRECTORY_NAME = "achievement_images"; 71 72 static constexpr u32 LEADERBOARD_NEARBY_ENTRIES_TO_FETCH = 10; 73 static constexpr u32 LEADERBOARD_ALL_FETCH_SIZE = 20; 74 75 static constexpr float LOGIN_NOTIFICATION_TIME = 5.0f; 76 static constexpr float ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME = 5.0f; 77 static constexpr float GAME_COMPLETE_NOTIFICATION_TIME = 20.0f; 78 static constexpr float LEADERBOARD_STARTED_NOTIFICATION_TIME = 3.0f; 79 static constexpr float LEADERBOARD_FAILED_NOTIFICATION_TIME = 3.0f; 80 81 static constexpr float INDICATOR_FADE_IN_TIME = 0.1f; 82 static constexpr float INDICATOR_FADE_OUT_TIME = 0.5f; 83 84 // Some API calls are really slow. Set a longer timeout. 85 static constexpr float SERVER_CALL_TIMEOUT = 60.0f; 86 87 // Chrome uses 10 server calls per domain, seems reasonable. 88 static constexpr u32 MAX_CONCURRENT_SERVER_CALLS = 10; 89 90 namespace { 91 struct LoginWithPasswordParameters 92 { 93 const char* username; 94 Error* error; 95 rc_client_async_handle_t* request; 96 bool result; 97 }; 98 struct LeaderboardTrackerIndicator 99 { 100 u32 tracker_id; 101 std::string text; 102 Common::Timer show_hide_time; 103 bool active; 104 }; 105 106 struct AchievementChallengeIndicator 107 { 108 const rc_client_achievement_t* achievement; 109 std::string badge_path; 110 Common::Timer show_hide_time; 111 bool active; 112 }; 113 114 struct AchievementProgressIndicator 115 { 116 const rc_client_achievement_t* achievement; 117 std::string badge_path; 118 Common::Timer show_hide_time; 119 bool active; 120 }; 121 } // namespace 122 123 static void ReportError(std::string_view sv); 124 template<typename... T> 125 static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args); 126 template<typename... T> 127 static void ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args); 128 static void ClearGameInfo(); 129 static void ClearGameHash(); 130 static std::string GetGameHash(CDImage* image); 131 static void SetHardcoreMode(bool enabled, bool force_display_message); 132 static bool IsLoggedInOrLoggingIn(); 133 static bool CanEnableHardcoreMode(); 134 static void ShowLoginSuccess(const rc_client_t* client); 135 static void ShowLoginNotification(); 136 static void IdentifyGame(const std::string& path, CDImage* image); 137 static void BeginLoadGame(); 138 static void BeginChangeDisc(); 139 static void UpdateGameSummary(); 140 static std::string GetLocalImagePath(const std::string_view image_name, int type); 141 static void DownloadImage(std::string url, std::string cache_filename); 142 static void UpdateGlyphRanges(); 143 144 static bool CreateClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http); 145 static void DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http); 146 static void ClientMessageCallback(const char* message, const rc_client_t* client); 147 static uint32_t ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); 148 static void ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, 149 rc_client_t* client); 150 151 static void ClientEventHandler(const rc_client_event_t* event, rc_client_t* client); 152 static void HandleResetEvent(const rc_client_event_t* event); 153 static void HandleUnlockEvent(const rc_client_event_t* event); 154 static void HandleGameCompleteEvent(const rc_client_event_t* event); 155 static void HandleLeaderboardStartedEvent(const rc_client_event_t* event); 156 static void HandleLeaderboardFailedEvent(const rc_client_event_t* event); 157 static void HandleLeaderboardSubmittedEvent(const rc_client_event_t* event); 158 static void HandleLeaderboardScoreboardEvent(const rc_client_event_t* event); 159 static void HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event); 160 static void HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event); 161 static void HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event); 162 static void HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event); 163 static void HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event); 164 static void HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event); 165 static void HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event); 166 static void HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event); 167 static void HandleServerErrorEvent(const rc_client_event_t* event); 168 static void HandleServerDisconnectedEvent(const rc_client_event_t* event); 169 static void HandleServerReconnectedEvent(const rc_client_event_t* event); 170 171 static void ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void* userdata); 172 static void ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, void* userdata); 173 static void ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata); 174 175 static void DisplayHardcoreDeferredMessage(); 176 static void DisplayAchievementSummary(); 177 static void UpdateRichPresence(std::unique_lock<std::recursive_mutex>& lock); 178 179 static void LeaderboardFetchNearbyCallback(int result, const char* error_message, 180 rc_client_leaderboard_entry_list_t* list, rc_client_t* client, 181 void* callback_userdata); 182 static void LeaderboardFetchAllCallback(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, 183 rc_client_t* client, void* callback_userdata); 184 185 #ifndef __ANDROID__ 186 static void DrawAchievement(const rc_client_achievement_t* cheevo); 187 static void DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard); 188 static void DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self, float rank_column_width, 189 float name_column_width, float time_column_width, float column_spacing); 190 #endif 191 192 static bool s_hardcore_mode = false; 193 194 #ifdef ENABLE_RAINTEGRATION 195 static bool s_using_raintegration = false; 196 #endif 197 198 static std::recursive_mutex s_achievements_mutex; 199 static rc_client_t* s_client; 200 static std::unique_ptr<HTTPDownloader> s_http_downloader; 201 202 static std::string s_game_path; 203 static std::string s_game_hash; 204 static std::string s_game_title; 205 static std::string s_game_icon; 206 static rc_client_user_game_summary_t s_game_summary; 207 static u32 s_game_id = 0; 208 static DynamicHeapArray<u8> s_state_buffer; 209 210 static bool s_has_achievements = false; 211 static bool s_has_leaderboards = false; 212 static bool s_has_rich_presence = false; 213 static std::string s_rich_presence_string; 214 static Common::Timer s_rich_presence_poll_time; 215 216 static rc_client_async_handle_t* s_login_request; 217 static rc_client_async_handle_t* s_load_game_request; 218 219 static rc_client_achievement_list_t* s_achievement_list; 220 static rc_client_leaderboard_list_t* s_leaderboard_list; 221 static std::vector<std::pair<const void*, std::string>> s_achievement_badge_paths; 222 static const rc_client_leaderboard_t* s_open_leaderboard = nullptr; 223 static rc_client_async_handle_t* s_leaderboard_fetch_handle = nullptr; 224 static std::vector<rc_client_leaderboard_entry_list_t*> s_leaderboard_entry_lists; 225 static rc_client_leaderboard_entry_list_t* s_leaderboard_nearby_entries; 226 static std::vector<std::pair<const rc_client_leaderboard_entry_t*, std::string>> s_leaderboard_user_icon_paths; 227 static bool s_is_showing_all_leaderboard_entries = false; 228 229 static std::vector<LeaderboardTrackerIndicator> s_active_leaderboard_trackers; 230 static std::vector<AchievementChallengeIndicator> s_active_challenge_indicators; 231 static std::optional<AchievementProgressIndicator> s_active_progress_indicator; 232 } // namespace Achievements 233 234 std::unique_lock<std::recursive_mutex> Achievements::GetLock() 235 { 236 return std::unique_lock(s_achievements_mutex); 237 } 238 239 rc_client_t* Achievements::GetClient() 240 { 241 return s_client; 242 } 243 244 const rc_client_user_game_summary_t& Achievements::GetGameSummary() 245 { 246 return s_game_summary; 247 } 248 249 void Achievements::ReportError(std::string_view sv) 250 { 251 std::string error = fmt::format("Achievements error: {}", sv); 252 ERROR_LOG(error.c_str()); 253 Host::AddOSDMessage(std::move(error), Host::OSD_CRITICAL_ERROR_DURATION); 254 } 255 256 template<typename... T> 257 void Achievements::ReportFmtError(fmt::format_string<T...> fmt, T&&... args) 258 { 259 TinyString str; 260 fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...)); 261 ReportError(str); 262 } 263 264 template<typename... T> 265 void Achievements::ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args) 266 { 267 TinyString str; 268 fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...)); 269 str.append_format("{} ({})", rc_error_str(err), err); 270 ReportError(str); 271 } 272 273 std::string Achievements::GetGameHash(CDImage* image) 274 { 275 std::string executable_name; 276 std::vector<u8> executable_data; 277 if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) 278 return {}; 279 280 BIOS::PSEXEHeader header = {}; 281 if (executable_data.size() >= sizeof(header)) 282 std::memcpy(&header, executable_data.data(), sizeof(header)); 283 if (!BIOS::IsValidPSExeHeader(header, executable_data.size())) 284 { 285 ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size()); 286 return {}; 287 } 288 289 // See rcheevos hash.c - rc_hash_psx(). 290 const u32 MAX_HASH_SIZE = 64 * 1024 * 1024; 291 const u32 hash_size = 292 std::min(std::min<u32>(sizeof(header) + header.file_size, MAX_HASH_SIZE), static_cast<u32>(executable_data.size())); 293 294 MD5Digest digest; 295 digest.Update(executable_name.c_str(), static_cast<u32>(executable_name.size())); 296 if (hash_size > 0) 297 digest.Update(executable_data); 298 299 u8 hash[16]; 300 digest.Final(hash); 301 302 const std::string hash_str = 303 fmt::format("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", 304 hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], 305 hash[11], hash[12], hash[13], hash[14], hash[15]); 306 307 INFO_LOG("Hash for '{}' ({} bytes, {} bytes hashed): {}", executable_name, executable_data.size(), hash_size, 308 hash_str); 309 return hash_str; 310 } 311 312 std::string Achievements::GetLocalImagePath(const std::string_view image_name, int type) 313 { 314 std::string_view prefix; 315 std::string_view suffix; 316 switch (type) 317 { 318 case RC_IMAGE_TYPE_GAME: 319 prefix = "image"; // https://media.retroachievements.org/Images/{}.png 320 break; 321 322 case RC_IMAGE_TYPE_USER: 323 prefix = "user"; // https://media.retroachievements.org/UserPic/{}.png 324 break; 325 326 case RC_IMAGE_TYPE_ACHIEVEMENT: // https://media.retroachievements.org/Badge/{}.png 327 prefix = "badge"; 328 break; 329 330 case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: 331 prefix = "badge"; 332 suffix = "_lock"; 333 break; 334 335 default: 336 prefix = "badge"; 337 break; 338 } 339 340 std::string ret; 341 if (!image_name.empty()) 342 { 343 ret = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}" FS_OSPATH_SEPARATOR_STR "{}_{}{}.png", EmuFolders::Cache, 344 CACHE_SUBDIRECTORY_NAME, prefix, Path::SanitizeFileName(image_name), suffix); 345 } 346 347 return ret; 348 } 349 350 void Achievements::DownloadImage(std::string url, std::string cache_filename) 351 { 352 auto callback = [cache_filename](s32 status_code, const std::string& content_type, 353 HTTPDownloader::Request::Data data) { 354 if (status_code != HTTPDownloader::HTTP_STATUS_OK) 355 return; 356 357 if (!FileSystem::WriteBinaryFile(cache_filename.c_str(), data.data(), data.size())) 358 { 359 ERROR_LOG("Failed to write badge image to '{}'", cache_filename); 360 return; 361 } 362 363 ImGuiFullscreen::InvalidateCachedTexture(cache_filename); 364 }; 365 366 s_http_downloader->CreateRequest(std::move(url), std::move(callback)); 367 } 368 369 void Achievements::UpdateGlyphRanges() 370 { 371 // To avoid rasterizing all emoji fonts, we get the set of used glyphs in the emoji range for all strings in the 372 // current game's achievement data. 373 using CodepointSet = std::unordered_set<ImGuiManager::WCharType>; 374 CodepointSet codepoints; 375 376 static constexpr auto add_string = [](const std::string_view str, CodepointSet& codepoints) { 377 char32_t codepoint; 378 for (size_t offset = 0; offset < str.length();) 379 { 380 offset += StringUtil::DecodeUTF8(str, offset, &codepoint); 381 382 // Basic Latin + Latin Supplement always included. 383 if (codepoint != StringUtil::UNICODE_REPLACEMENT_CHARACTER && codepoint >= 0x2000) 384 codepoints.insert(static_cast<ImGuiManager::WCharType>(codepoint)); 385 } 386 }; 387 388 if (rc_client_has_rich_presence(s_client)) 389 { 390 std::vector<const char*> rp_strings; 391 for (;;) 392 { 393 rp_strings.resize(std::max<size_t>(rp_strings.size() * 2, 512)); 394 395 size_t count; 396 const int err = rc_client_get_rich_presence_strings(s_client, rp_strings.data(), rp_strings.size(), &count); 397 if (err == RC_INSUFFICIENT_BUFFER) 398 continue; 399 else if (err != RC_OK) 400 rp_strings.clear(); 401 else 402 rp_strings.resize(count); 403 404 break; 405 } 406 407 for (const char* str : rp_strings) 408 add_string(str, codepoints); 409 } 410 411 if (rc_client_has_achievements(s_client)) 412 { 413 rc_client_achievement_list_t* const achievements = 414 rc_client_create_achievement_list(s_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, 0); 415 if (achievements) 416 { 417 for (u32 i = 0; i < achievements->num_buckets; i++) 418 { 419 const rc_client_achievement_bucket_t& bucket = achievements->buckets[i]; 420 for (u32 j = 0; j < bucket.num_achievements; j++) 421 { 422 const rc_client_achievement_t* achievement = bucket.achievements[j]; 423 if (achievement->title) 424 add_string(achievement->title, codepoints); 425 if (achievement->description) 426 add_string(achievement->description, codepoints); 427 } 428 } 429 rc_client_destroy_achievement_list(achievements); 430 } 431 } 432 433 if (rc_client_has_leaderboards(s_client)) 434 { 435 rc_client_leaderboard_list_t* const leaderboards = 436 rc_client_create_leaderboard_list(s_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); 437 if (leaderboards) 438 { 439 for (u32 i = 0; i < leaderboards->num_buckets; i++) 440 { 441 const rc_client_leaderboard_bucket_t& bucket = leaderboards->buckets[i]; 442 for (u32 j = 0; j < bucket.num_leaderboards; j++) 443 { 444 const rc_client_leaderboard_t* leaderboard = bucket.leaderboards[j]; 445 if (leaderboard->title) 446 add_string(leaderboard->title, codepoints); 447 if (leaderboard->description) 448 add_string(leaderboard->description, codepoints); 449 } 450 } 451 rc_client_destroy_leaderboard_list(leaderboards); 452 } 453 } 454 455 std::vector<ImGuiManager::WCharType> sorted_codepoints; 456 sorted_codepoints.reserve(codepoints.size()); 457 sorted_codepoints.insert(sorted_codepoints.begin(), codepoints.begin(), codepoints.end()); 458 std::sort(sorted_codepoints.begin(), sorted_codepoints.end()); 459 460 // Compact codepoints to ranges. 461 ImGuiManager::SetEmojiFontRange(ImGuiManager::CompactFontRange(sorted_codepoints)); 462 } 463 464 bool Achievements::IsActive() 465 { 466 #ifdef ENABLE_RAINTEGRATION 467 return (s_client != nullptr) || s_using_raintegration; 468 #else 469 return (s_client != nullptr); 470 #endif 471 } 472 473 bool Achievements::IsHardcoreModeActive() 474 { 475 #ifdef ENABLE_RAINTEGRATION 476 if (IsUsingRAIntegration()) 477 return RA_HardcoreModeIsActive() != 0; 478 #endif 479 480 return s_hardcore_mode; 481 } 482 483 bool Achievements::HasActiveGame() 484 { 485 return s_game_id != 0; 486 } 487 488 u32 Achievements::GetGameID() 489 { 490 return s_game_id; 491 } 492 493 bool Achievements::HasAchievementsOrLeaderboards() 494 { 495 return s_has_achievements || s_has_leaderboards; 496 } 497 498 bool Achievements::HasAchievements() 499 { 500 return s_has_achievements; 501 } 502 503 bool Achievements::HasLeaderboards() 504 { 505 return s_has_leaderboards; 506 } 507 508 bool Achievements::HasRichPresence() 509 { 510 return s_has_rich_presence; 511 } 512 513 const std::string& Achievements::GetGameTitle() 514 { 515 return s_game_title; 516 } 517 518 const std::string& Achievements::GetGameIconPath() 519 { 520 return s_game_icon; 521 } 522 523 const std::string& Achievements::GetRichPresenceString() 524 { 525 return s_rich_presence_string; 526 } 527 528 bool Achievements::Initialize() 529 { 530 if (IsUsingRAIntegration()) 531 return true; 532 533 auto lock = GetLock(); 534 AssertMsg(g_settings.achievements_enabled, "Achievements are enabled"); 535 Assert(!s_client && !s_http_downloader); 536 537 if (!CreateClient(&s_client, &s_http_downloader)) 538 return false; 539 540 // Hardcore starts off. We enable it on first boot. 541 s_hardcore_mode = false; 542 543 rc_client_set_event_handler(s_client, ClientEventHandler); 544 545 rc_client_set_hardcore_enabled(s_client, s_hardcore_mode); 546 rc_client_set_encore_mode_enabled(s_client, g_settings.achievements_encore_mode); 547 rc_client_set_unofficial_enabled(s_client, g_settings.achievements_unofficial_test_mode); 548 rc_client_set_spectator_mode_enabled(s_client, g_settings.achievements_spectator_mode); 549 550 // Begin disc identification early, before the login finishes. 551 if (System::IsValid()) 552 IdentifyGame(System::GetDiscPath(), nullptr); 553 554 std::string username = Host::GetBaseStringSettingValue("Cheevos", "Username"); 555 std::string api_token = Host::GetBaseStringSettingValue("Cheevos", "Token"); 556 if (!username.empty() && !api_token.empty()) 557 { 558 INFO_LOG("Attempting login with user '{}'...", username); 559 s_login_request = rc_client_begin_login_with_token(s_client, username.c_str(), api_token.c_str(), 560 ClientLoginWithTokenCallback, nullptr); 561 } 562 563 // Hardcore mode isn't enabled when achievements first starts, if a game is already running. 564 if (System::IsValid() && IsLoggedInOrLoggingIn() && g_settings.achievements_hardcore_mode) 565 DisplayHardcoreDeferredMessage(); 566 567 return true; 568 } 569 570 bool Achievements::CreateClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http) 571 { 572 *http = HTTPDownloader::Create(Host::GetHTTPUserAgent()); 573 if (!*http) 574 { 575 Host::ReportErrorAsync("Achievements Error", "Failed to create HTTPDownloader, cannot use achievements"); 576 return false; 577 } 578 579 (*http)->SetTimeout(SERVER_CALL_TIMEOUT); 580 (*http)->SetMaxActiveRequests(MAX_CONCURRENT_SERVER_CALLS); 581 582 rc_client_t* new_client = rc_client_create(ClientReadMemory, ClientServerCall); 583 if (!new_client) 584 { 585 Host::ReportErrorAsync("Achievements Error", "rc_client_create() failed, cannot use achievements"); 586 http->reset(); 587 return false; 588 } 589 590 #ifdef _DEBUG 591 rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_VERBOSE, ClientMessageCallback); 592 #else 593 rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_INFO, ClientMessageCallback); 594 #endif 595 596 rc_client_set_userdata(new_client, http->get()); 597 598 *client = new_client; 599 return true; 600 } 601 602 void Achievements::DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http) 603 { 604 (*http)->WaitForAllRequests(); 605 606 rc_client_destroy(*client); 607 *client = nullptr; 608 609 http->reset(); 610 } 611 612 void Achievements::UpdateSettings(const Settings& old_config) 613 { 614 if (IsUsingRAIntegration()) 615 return; 616 617 if (!g_settings.achievements_enabled) 618 { 619 // we're done here 620 Shutdown(false); 621 return; 622 } 623 624 if (!IsActive()) 625 { 626 // we just got enabled 627 Initialize(); 628 return; 629 } 630 631 if (g_settings.achievements_hardcore_mode != old_config.achievements_hardcore_mode) 632 { 633 // Hardcore mode can only be enabled through reset (ResetChallengeMode()). 634 if (s_hardcore_mode && !g_settings.achievements_hardcore_mode) 635 { 636 ResetHardcoreMode(false); 637 } 638 else if (!s_hardcore_mode && g_settings.achievements_hardcore_mode) 639 { 640 if (HasActiveGame()) 641 DisplayHardcoreDeferredMessage(); 642 } 643 } 644 645 // These cannot be modified while a game is loaded, so just toss state and reload. 646 if (HasActiveGame()) 647 { 648 if (g_settings.achievements_encore_mode != old_config.achievements_encore_mode || 649 g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode || 650 g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode) 651 { 652 Shutdown(false); 653 Initialize(); 654 return; 655 } 656 } 657 else 658 { 659 if (g_settings.achievements_encore_mode != old_config.achievements_encore_mode) 660 rc_client_set_encore_mode_enabled(s_client, g_settings.achievements_encore_mode); 661 if (g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode) 662 rc_client_set_spectator_mode_enabled(s_client, g_settings.achievements_spectator_mode); 663 if (g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode) 664 rc_client_set_unofficial_enabled(s_client, g_settings.achievements_unofficial_test_mode); 665 } 666 } 667 668 bool Achievements::Shutdown(bool allow_cancel) 669 { 670 #ifdef ENABLE_RAINTEGRATION 671 if (IsUsingRAIntegration()) 672 { 673 if (System::IsValid() && allow_cancel && !RA_ConfirmLoadNewRom(true)) 674 return false; 675 676 RA_SetPaused(false); 677 RA_ActivateGame(0); 678 return true; 679 } 680 #endif 681 682 if (!IsActive()) 683 return true; 684 685 auto lock = GetLock(); 686 Assert(s_client && s_http_downloader); 687 688 ClearGameInfo(); 689 ClearGameHash(); 690 DisableHardcoreMode(); 691 UpdateGlyphRanges(); 692 693 if (s_load_game_request) 694 { 695 rc_client_abort_async(s_client, s_load_game_request); 696 s_load_game_request = nullptr; 697 } 698 if (s_login_request) 699 { 700 rc_client_abort_async(s_client, s_login_request); 701 s_login_request = nullptr; 702 } 703 704 s_hardcore_mode = false; 705 DestroyClient(&s_client, &s_http_downloader); 706 707 Host::OnAchievementsRefreshed(); 708 return true; 709 } 710 711 void Achievements::ClientMessageCallback(const char* message, const rc_client_t* client) 712 { 713 DEV_LOG(message); 714 } 715 716 uint32_t Achievements::ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) 717 { 718 if ((address + num_bytes) > 0x200400U) [[unlikely]] 719 return 0; 720 721 const u8* src = (address >= 0x200000U) ? CPU::g_state.scratchpad.data() : Bus::g_ram; 722 const u32 offset = (address & Bus::RAM_2MB_MASK); // size guarded by check above 723 724 switch (num_bytes) 725 { 726 case 1: 727 std::memcpy(buffer, &src[offset], 1); 728 break; 729 case 2: 730 std::memcpy(buffer, &src[offset], 2); 731 break; 732 case 4: 733 std::memcpy(buffer, &src[offset], 4); 734 break; 735 default: 736 [[unlikely]] std::memcpy(buffer, &src[offset], num_bytes); 737 break; 738 } 739 740 return num_bytes; 741 } 742 743 void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, 744 void* callback_data, rc_client_t* client) 745 { 746 HTTPDownloader::Request::Callback hd_callback = 747 [callback, callback_data](s32 status_code, const std::string& content_type, HTTPDownloader::Request::Data data) { 748 rc_api_server_response_t rr; 749 rr.http_status_code = (status_code <= 0) ? (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED ? 750 RC_API_SERVER_RESPONSE_CLIENT_ERROR : 751 RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) : 752 status_code; 753 rr.body_length = data.size(); 754 rr.body = reinterpret_cast<const char*>(data.data()); 755 756 callback(&rr, callback_data); 757 }; 758 759 HTTPDownloader* http = static_cast<HTTPDownloader*>(rc_client_get_userdata(client)); 760 761 // TODO: Content-type for post 762 if (request->post_data) 763 { 764 // const auto pd = std::string_view(request->post_data); 765 // Log_DevFmt("Server POST: {}", pd.substr(0, std::min<size_t>(pd.length(), 10))); 766 http->CreatePostRequest(request->url, request->post_data, std::move(hd_callback)); 767 } 768 else 769 { 770 http->CreateRequest(request->url, std::move(hd_callback)); 771 } 772 } 773 774 void Achievements::IdleUpdate() 775 { 776 if (!IsActive()) 777 return; 778 779 #ifdef ENABLE_RAINTEGRATION 780 if (IsUsingRAIntegration()) 781 return; 782 #endif 783 784 const auto lock = GetLock(); 785 786 s_http_downloader->PollRequests(); 787 rc_client_idle(s_client); 788 } 789 790 bool Achievements::NeedsIdleUpdate() 791 { 792 if (!IsActive()) 793 return false; 794 795 const auto lock = GetLock(); 796 return (s_http_downloader && s_http_downloader->HasAnyRequests()); 797 } 798 799 void Achievements::FrameUpdate() 800 { 801 if (!IsActive()) 802 return; 803 804 #ifdef ENABLE_RAINTEGRATION 805 if (IsUsingRAIntegration()) 806 { 807 RA_DoAchievementsFrame(); 808 return; 809 } 810 #endif 811 812 auto lock = GetLock(); 813 814 s_http_downloader->PollRequests(); 815 rc_client_do_frame(s_client); 816 817 UpdateRichPresence(lock); 818 } 819 820 void Achievements::ClientEventHandler(const rc_client_event_t* event, rc_client_t* client) 821 { 822 switch (event->type) 823 { 824 case RC_CLIENT_EVENT_RESET: 825 HandleResetEvent(event); 826 break; 827 828 case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: 829 HandleUnlockEvent(event); 830 break; 831 832 case RC_CLIENT_EVENT_GAME_COMPLETED: 833 HandleGameCompleteEvent(event); 834 break; 835 836 case RC_CLIENT_EVENT_LEADERBOARD_STARTED: 837 HandleLeaderboardStartedEvent(event); 838 break; 839 840 case RC_CLIENT_EVENT_LEADERBOARD_FAILED: 841 HandleLeaderboardFailedEvent(event); 842 break; 843 844 case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: 845 HandleLeaderboardSubmittedEvent(event); 846 break; 847 848 case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: 849 HandleLeaderboardScoreboardEvent(event); 850 break; 851 852 case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: 853 HandleLeaderboardTrackerShowEvent(event); 854 break; 855 856 case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: 857 HandleLeaderboardTrackerHideEvent(event); 858 break; 859 860 case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: 861 HandleLeaderboardTrackerUpdateEvent(event); 862 break; 863 864 case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: 865 HandleAchievementChallengeIndicatorShowEvent(event); 866 break; 867 868 case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: 869 HandleAchievementChallengeIndicatorHideEvent(event); 870 break; 871 872 case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: 873 HandleAchievementProgressIndicatorShowEvent(event); 874 break; 875 876 case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: 877 HandleAchievementProgressIndicatorHideEvent(event); 878 break; 879 880 case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: 881 HandleAchievementProgressIndicatorUpdateEvent(event); 882 break; 883 884 case RC_CLIENT_EVENT_SERVER_ERROR: 885 HandleServerErrorEvent(event); 886 break; 887 888 case RC_CLIENT_EVENT_DISCONNECTED: 889 HandleServerDisconnectedEvent(event); 890 break; 891 892 case RC_CLIENT_EVENT_RECONNECTED: 893 HandleServerReconnectedEvent(event); 894 break; 895 896 default: 897 [[unlikely]] ERROR_LOG("Unhandled event: {}", event->type); 898 break; 899 } 900 } 901 902 void Achievements::UpdateGameSummary() 903 { 904 rc_client_get_user_game_summary(s_client, &s_game_summary); 905 } 906 907 void Achievements::UpdateRichPresence(std::unique_lock<std::recursive_mutex>& lock) 908 { 909 // Limit rich presence updates to once per second, since it could change per frame. 910 if (!s_has_rich_presence || !s_rich_presence_poll_time.ResetIfSecondsPassed(1.0)) 911 return; 912 913 char buffer[512]; 914 const size_t res = rc_client_get_rich_presence_message(s_client, buffer, std::size(buffer)); 915 const std::string_view sv(buffer, res); 916 if (s_rich_presence_string == sv) 917 return; 918 919 s_rich_presence_string.assign(sv); 920 921 INFO_LOG("Rich presence updated: {}", s_rich_presence_string); 922 Host::OnAchievementsRefreshed(); 923 924 lock.unlock(); 925 System::UpdateRichPresence(false); 926 lock.lock(); 927 } 928 929 void Achievements::GameChanged(const std::string& path, CDImage* image) 930 { 931 std::unique_lock lock(s_achievements_mutex); 932 933 if (!IsActive()) 934 return; 935 936 IdentifyGame(path, image); 937 } 938 939 void Achievements::IdentifyGame(const std::string& path, CDImage* image) 940 { 941 if (s_game_path == path) 942 { 943 WARNING_LOG("Game path is unchanged."); 944 return; 945 } 946 947 std::unique_ptr<CDImage> temp_image; 948 if (!path.empty() && (!image || (g_settings.achievements_use_first_disc_from_playlist && image->HasSubImages() && 949 image->GetCurrentSubImage() != 0))) 950 { 951 temp_image = CDImage::Open(path.c_str(), g_settings.cdrom_load_image_patches, nullptr); 952 image = temp_image.get(); 953 if (!temp_image) 954 ERROR_LOG("Failed to open temporary CD image '{}'", path); 955 } 956 957 std::string game_hash; 958 if (image) 959 game_hash = GetGameHash(image); 960 961 if (s_game_hash == game_hash) 962 { 963 // only the path has changed - different format/save state/etc. 964 INFO_LOG("Detected path change from '{}' to '{}'", s_game_path, path); 965 s_game_path = path; 966 return; 967 } 968 969 ClearGameHash(); 970 s_game_path = path; 971 s_game_hash = std::move(game_hash); 972 s_state_buffer.deallocate(); 973 974 #ifdef ENABLE_RAINTEGRATION 975 if (IsUsingRAIntegration()) 976 { 977 RAIntegration::GameChanged(); 978 return; 979 } 980 #endif 981 982 // shouldn't have a load game request when we're not logged in. 983 Assert(IsLoggedInOrLoggingIn() || !s_load_game_request); 984 985 // bail out if we're not logged in, just save the hash 986 if (!IsLoggedInOrLoggingIn()) 987 { 988 INFO_LOG("Skipping load game because we're not logged in."); 989 DisableHardcoreMode(); 990 return; 991 } 992 993 if (!rc_client_is_game_loaded(s_client)) 994 BeginLoadGame(); 995 else 996 BeginChangeDisc(); 997 } 998 999 void Achievements::BeginLoadGame() 1000 { 1001 ClearGameInfo(); 1002 1003 if (s_game_hash.empty()) 1004 { 1005 // when we're booting the bios, this will fail 1006 if (!s_game_path.empty()) 1007 { 1008 Host::AddKeyedOSDMessage( 1009 "retroachievements_disc_read_failed", 1010 TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."), 1011 Host::OSD_ERROR_DURATION); 1012 } 1013 1014 DisableHardcoreMode(); 1015 UpdateGlyphRanges(); 1016 return; 1017 } 1018 1019 s_load_game_request = rc_client_begin_load_game(s_client, s_game_hash.c_str(), ClientLoadGameCallback, nullptr); 1020 } 1021 1022 void Achievements::BeginChangeDisc() 1023 { 1024 // cancel previous requests 1025 if (s_load_game_request) 1026 { 1027 rc_client_abort_async(s_client, s_load_game_request); 1028 s_load_game_request = nullptr; 1029 } 1030 1031 if (s_game_hash.empty()) 1032 { 1033 // when we're booting the bios, this will fail 1034 if (!s_game_path.empty()) 1035 { 1036 Host::AddKeyedOSDMessage( 1037 "retroachievements_disc_read_failed", 1038 TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."), 1039 Host::OSD_ERROR_DURATION); 1040 } 1041 1042 ClearGameInfo(); 1043 DisableHardcoreMode(); 1044 UpdateGlyphRanges(); 1045 return; 1046 } 1047 1048 s_load_game_request = rc_client_begin_change_media_from_hash(s_client, s_game_hash.c_str(), ClientLoadGameCallback, 1049 reinterpret_cast<void*>(static_cast<uintptr_t>(1))); 1050 } 1051 1052 void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) 1053 { 1054 const bool was_disc_change = (userdata != nullptr); 1055 1056 s_load_game_request = nullptr; 1057 s_state_buffer.deallocate(); 1058 1059 if (result == RC_NO_GAME_LOADED) 1060 { 1061 // Unknown game. 1062 INFO_LOG("Unknown game '{}', disabling achievements.", s_game_hash); 1063 if (was_disc_change) 1064 { 1065 ClearGameInfo(); 1066 UpdateGlyphRanges(); 1067 } 1068 1069 DisableHardcoreMode(); 1070 return; 1071 } 1072 else if (result == RC_LOGIN_REQUIRED) 1073 { 1074 // We would've asked to re-authenticate, so leave HC on for now. 1075 // Once we've done so, we'll reload the game. 1076 return; 1077 } 1078 else if (result != RC_OK) 1079 { 1080 ReportFmtError("Loading game failed: {}", error_message); 1081 if (was_disc_change) 1082 { 1083 ClearGameInfo(); 1084 UpdateGlyphRanges(); 1085 } 1086 1087 DisableHardcoreMode(); 1088 return; 1089 } 1090 else if (result == RC_HARDCORE_DISABLED) 1091 { 1092 if (error_message) 1093 ReportError(error_message); 1094 1095 DisableHardcoreMode(); 1096 } 1097 1098 const rc_client_game_t* info = rc_client_get_game_info(s_client); 1099 if (!info) 1100 { 1101 ReportError("rc_client_get_game_info() returned NULL"); 1102 if (was_disc_change) 1103 { 1104 ClearGameInfo(); 1105 UpdateGlyphRanges(); 1106 } 1107 1108 DisableHardcoreMode(); 1109 return; 1110 } 1111 1112 const bool has_achievements = rc_client_has_achievements(client); 1113 const bool has_leaderboards = rc_client_has_leaderboards(client); 1114 1115 // Only display summary if the game title has changed across discs. 1116 const bool display_summary = (s_game_id != info->id || s_game_title != info->title); 1117 1118 // If the game has a RetroAchievements entry but no achievements or leaderboards, 1119 // enforcing hardcore mode is pointless. 1120 if (!has_achievements && !has_leaderboards) 1121 DisableHardcoreMode(); 1122 1123 // We should have matched hardcore mode state. 1124 Assert(s_hardcore_mode == (rc_client_get_hardcore_enabled(client) != 0)); 1125 1126 s_game_id = info->id; 1127 s_game_title = info->title; 1128 s_has_achievements = has_achievements; 1129 s_has_leaderboards = has_leaderboards; 1130 s_has_rich_presence = rc_client_has_rich_presence(client); 1131 1132 // update ranges before initializing fsui 1133 UpdateGlyphRanges(); 1134 1135 // ensure fullscreen UI is ready for notifications 1136 if (display_summary) 1137 FullscreenUI::Initialize(); 1138 1139 s_game_icon = GetLocalImagePath(info->badge_name, RC_IMAGE_TYPE_GAME); 1140 if (!s_game_icon.empty() && !FileSystem::FileExists(s_game_icon.c_str())) 1141 { 1142 char buf[512]; 1143 if (int err = rc_client_game_get_image_url(info, buf, std::size(buf)); err == RC_OK) 1144 { 1145 DownloadImage(buf, s_game_icon); 1146 } 1147 else 1148 { 1149 ReportRCError(err, "rc_client_game_get_image_url() failed: "); 1150 } 1151 } 1152 1153 UpdateGameSummary(); 1154 if (display_summary) 1155 DisplayAchievementSummary(); 1156 1157 Host::OnAchievementsRefreshed(); 1158 } 1159 1160 void Achievements::ClearGameInfo() 1161 { 1162 ClearUIState(); 1163 1164 if (s_load_game_request) 1165 { 1166 rc_client_abort_async(s_client, s_load_game_request); 1167 s_load_game_request = nullptr; 1168 } 1169 rc_client_unload_game(s_client); 1170 1171 s_active_leaderboard_trackers = {}; 1172 s_active_challenge_indicators = {}; 1173 s_active_progress_indicator.reset(); 1174 s_game_id = 0; 1175 s_game_title = {}; 1176 s_game_icon = {}; 1177 s_state_buffer.deallocate(); 1178 s_has_achievements = false; 1179 s_has_leaderboards = false; 1180 s_has_rich_presence = false; 1181 s_rich_presence_string = {}; 1182 s_game_summary = {}; 1183 1184 Host::OnAchievementsRefreshed(); 1185 } 1186 1187 void Achievements::ClearGameHash() 1188 { 1189 s_game_path = {}; 1190 std::string().swap(s_game_hash); 1191 } 1192 1193 void Achievements::DisplayAchievementSummary() 1194 { 1195 if (g_settings.achievements_notifications && FullscreenUI::Initialize()) 1196 { 1197 std::string title; 1198 if (IsHardcoreModeActive()) 1199 title = fmt::format(TRANSLATE_FS("Achievements", "{} (Hardcore Mode)"), s_game_title); 1200 else 1201 title = s_game_title; 1202 1203 std::string summary; 1204 if (s_game_summary.num_core_achievements > 0) 1205 { 1206 summary = fmt::format( 1207 TRANSLATE_FS("Achievements", "{0}, {1}."), 1208 SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "You have unlocked {} of %n achievements", 1209 "Achievement popup", s_game_summary.num_core_achievements), 1210 s_game_summary.num_unlocked_achievements), 1211 SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "and earned {} of %n points", "Achievement popup", 1212 s_game_summary.points_core), 1213 s_game_summary.points_unlocked)); 1214 } 1215 else 1216 { 1217 summary = TRANSLATE_STR("Achievements", "This game has no achievements."); 1218 } 1219 1220 ImGuiFullscreen::AddNotification("achievement_summary", ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, std::move(title), 1221 std::move(summary), s_game_icon); 1222 } 1223 1224 // Technically not going through the resource API, but since we're passing this to something else, we can't. 1225 if (g_settings.achievements_sound_effects) 1226 PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(INFO_SOUND_NAME).c_str()); 1227 } 1228 1229 void Achievements::DisplayHardcoreDeferredMessage() 1230 { 1231 if (g_settings.achievements_hardcore_mode && !s_hardcore_mode && System::IsValid() && FullscreenUI::Initialize()) 1232 { 1233 ImGuiFullscreen::ShowToast(std::string(), 1234 TRANSLATE_STR("Achievements", "Hardcore mode will be enabled on system reset."), 1235 Host::OSD_WARNING_DURATION); 1236 } 1237 } 1238 1239 void Achievements::HandleResetEvent(const rc_client_event_t* event) 1240 { 1241 // We handle system resets ourselves, but still need to reset the client's state. 1242 INFO_LOG("Resetting runtime due to reset event"); 1243 rc_client_reset(s_client); 1244 1245 if (HasActiveGame()) 1246 UpdateGameSummary(); 1247 } 1248 1249 void Achievements::HandleUnlockEvent(const rc_client_event_t* event) 1250 { 1251 const rc_client_achievement_t* cheevo = event->achievement; 1252 DebugAssert(cheevo); 1253 1254 INFO_LOG("Achievement {} ({}) for game {} unlocked", cheevo->title, cheevo->id, s_game_id); 1255 UpdateGameSummary(); 1256 1257 if (g_settings.achievements_notifications && FullscreenUI::Initialize()) 1258 { 1259 std::string title; 1260 if (cheevo->category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) 1261 title = fmt::format(TRANSLATE_FS("Achievements", "{} (Unofficial)"), cheevo->title); 1262 else 1263 title = cheevo->title; 1264 1265 std::string badge_path = GetAchievementBadgePath(cheevo, cheevo->state); 1266 1267 ImGuiFullscreen::AddNotification(fmt::format("achievement_unlock_{}", cheevo->id), 1268 static_cast<float>(g_settings.achievements_notification_duration), 1269 std::move(title), cheevo->description, std::move(badge_path)); 1270 } 1271 1272 if (g_settings.achievements_sound_effects) 1273 PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(UNLOCK_SOUND_NAME).c_str()); 1274 } 1275 1276 void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event) 1277 { 1278 INFO_LOG("Game {} complete", s_game_id); 1279 UpdateGameSummary(); 1280 1281 if (g_settings.achievements_notifications && FullscreenUI::Initialize()) 1282 { 1283 std::string title = fmt::format(TRANSLATE_FS("Achievements", "Mastered {}"), s_game_title); 1284 std::string message = fmt::format( 1285 TRANSLATE_FS("Achievements", "{0}, {1}"), 1286 TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup", 1287 s_game_summary.num_unlocked_achievements), 1288 TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_game_summary.points_unlocked)); 1289 1290 ImGuiFullscreen::AddNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, std::move(title), 1291 std::move(message), s_game_icon); 1292 } 1293 } 1294 1295 void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event) 1296 { 1297 DEV_LOG("Leaderboard {} ({}) started", event->leaderboard->id, event->leaderboard->title); 1298 1299 if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) 1300 { 1301 std::string title = event->leaderboard->title; 1302 std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt started."); 1303 1304 ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), 1305 LEADERBOARD_STARTED_NOTIFICATION_TIME, std::move(title), std::move(message), 1306 s_game_icon); 1307 } 1308 } 1309 1310 void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event) 1311 { 1312 DEV_LOG("Leaderboard {} ({}) failed", event->leaderboard->id, event->leaderboard->title); 1313 1314 if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) 1315 { 1316 std::string title = event->leaderboard->title; 1317 std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt failed."); 1318 1319 ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), 1320 LEADERBOARD_FAILED_NOTIFICATION_TIME, std::move(title), std::move(message), 1321 s_game_icon); 1322 } 1323 } 1324 1325 void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* event) 1326 { 1327 DEV_LOG("Leaderboard {} ({}) submitted", event->leaderboard->id, event->leaderboard->title); 1328 1329 if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) 1330 { 1331 static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { 1332 TRANSLATE_NOOP("Achievements", "Your Time: {}{}"), 1333 TRANSLATE_NOOP("Achievements", "Your Score: {}{}"), 1334 TRANSLATE_NOOP("Achievements", "Your Value: {}{}"), 1335 }; 1336 1337 std::string title = event->leaderboard->title; 1338 std::string message = fmt::format( 1339 fmt::runtime(Host::TranslateToStringView( 1340 "Achievements", 1341 value_strings[std::min<u8>(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), 1342 event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown", 1343 g_settings.achievements_spectator_mode ? std::string_view() : TRANSLATE_SV("Achievements", " (Submitting)")); 1344 1345 ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), 1346 static_cast<float>(g_settings.achievements_leaderboard_duration), std::move(title), 1347 std::move(message), s_game_icon); 1348 } 1349 1350 if (g_settings.achievements_sound_effects) 1351 PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(LBSUBMIT_SOUND_NAME).c_str()); 1352 } 1353 1354 void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* event) 1355 { 1356 DEV_LOG("Leaderboard {} scoreboard rank {} of {}", event->leaderboard_scoreboard->leaderboard_id, 1357 event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries); 1358 1359 if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) 1360 { 1361 static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { 1362 TRANSLATE_NOOP("Achievements", "Your Time: {} (Best: {})"), 1363 TRANSLATE_NOOP("Achievements", "Your Score: {} (Best: {})"), 1364 TRANSLATE_NOOP("Achievements", "Your Value: {} (Best: {})"), 1365 }; 1366 1367 std::string title = event->leaderboard->title; 1368 std::string message = fmt::format( 1369 TRANSLATE_FS("Achievements", "{}\nLeaderboard Position: {} of {}"), 1370 fmt::format(fmt::runtime(Host::TranslateToStringView( 1371 "Achievements", 1372 value_strings[std::min<u8>(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), 1373 event->leaderboard_scoreboard->submitted_score, event->leaderboard_scoreboard->best_score), 1374 event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries); 1375 1376 ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), 1377 static_cast<float>(g_settings.achievements_leaderboard_duration), std::move(title), 1378 std::move(message), s_game_icon); 1379 } 1380 } 1381 1382 void Achievements::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event) 1383 { 1384 DEV_LOG("Showing leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display); 1385 1386 TinyString width_string; 1387 width_string.append(ICON_FA_STOPWATCH); 1388 const u32 display_len = static_cast<u32>(std::strlen(event->leaderboard_tracker->display)); 1389 for (u32 i = 0; i < display_len; i++) 1390 width_string.append('0'); 1391 1392 LeaderboardTrackerIndicator indicator; 1393 indicator.tracker_id = event->leaderboard_tracker->id; 1394 indicator.text = event->leaderboard_tracker->display; 1395 indicator.active = true; 1396 s_active_leaderboard_trackers.push_back(std::move(indicator)); 1397 } 1398 1399 void Achievements::HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event) 1400 { 1401 const u32 id = event->leaderboard_tracker->id; 1402 auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(), 1403 [id](const auto& it) { return it.tracker_id == id; }); 1404 if (it == s_active_leaderboard_trackers.end()) 1405 return; 1406 1407 DEV_LOG("Hiding leaderboard tracker: {}", id); 1408 it->active = false; 1409 it->show_hide_time.Reset(); 1410 } 1411 1412 void Achievements::HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event) 1413 { 1414 const u32 id = event->leaderboard_tracker->id; 1415 auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(), 1416 [id](const auto& it) { return it.tracker_id == id; }); 1417 if (it == s_active_leaderboard_trackers.end()) 1418 return; 1419 1420 DEV_LOG("Updating leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display); 1421 1422 it->text = event->leaderboard_tracker->display; 1423 it->active = true; 1424 } 1425 1426 void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event) 1427 { 1428 if (auto it = 1429 std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(), 1430 [event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; }); 1431 it != s_active_challenge_indicators.end()) 1432 { 1433 it->show_hide_time.Reset(); 1434 it->active = true; 1435 return; 1436 } 1437 1438 AchievementChallengeIndicator indicator; 1439 indicator.achievement = event->achievement; 1440 indicator.badge_path = GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); 1441 indicator.active = true; 1442 s_active_challenge_indicators.push_back(std::move(indicator)); 1443 1444 DEV_LOG("Show challenge indicator for {} ({})", event->achievement->id, event->achievement->title); 1445 } 1446 1447 void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event) 1448 { 1449 auto it = 1450 std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(), 1451 [event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; }); 1452 if (it == s_active_challenge_indicators.end()) 1453 return; 1454 1455 DEV_LOG("Hide challenge indicator for {} ({})", event->achievement->id, event->achievement->title); 1456 it->show_hide_time.Reset(); 1457 it->active = false; 1458 } 1459 1460 void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event) 1461 { 1462 DEV_LOG("Showing progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title, 1463 event->achievement->measured_progress); 1464 1465 if (!s_active_progress_indicator.has_value()) 1466 s_active_progress_indicator.emplace(); 1467 else 1468 s_active_progress_indicator->show_hide_time.Reset(); 1469 1470 s_active_progress_indicator->achievement = event->achievement; 1471 s_active_progress_indicator->badge_path = 1472 GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); 1473 s_active_progress_indicator->active = true; 1474 } 1475 1476 void Achievements::HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event) 1477 { 1478 if (!s_active_progress_indicator.has_value()) 1479 return; 1480 1481 DEV_LOG("Hiding progress indicator"); 1482 s_active_progress_indicator->show_hide_time.Reset(); 1483 s_active_progress_indicator->active = false; 1484 } 1485 1486 void Achievements::HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event) 1487 { 1488 DEV_LOG("Updating progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title, 1489 event->achievement->measured_progress); 1490 s_active_progress_indicator->achievement = event->achievement; 1491 s_active_progress_indicator->active = true; 1492 } 1493 1494 void Achievements::HandleServerErrorEvent(const rc_client_event_t* event) 1495 { 1496 std::string message = 1497 fmt::format(TRANSLATE_FS("Achievements", "Server error in {}:\n{}"), 1498 event->server_error->api ? event->server_error->api : "UNKNOWN", 1499 event->server_error->error_message ? event->server_error->error_message : "UNKNOWN"); 1500 ERROR_LOG(message.c_str()); 1501 Host::AddOSDMessage(std::move(message), Host::OSD_ERROR_DURATION); 1502 } 1503 1504 void Achievements::HandleServerDisconnectedEvent(const rc_client_event_t* event) 1505 { 1506 WARNING_LOG("Server disconnected."); 1507 1508 if (FullscreenUI::Initialize()) 1509 { 1510 ImGuiFullscreen::ShowToast( 1511 TRANSLATE_STR("Achievements", "Achievements Disconnected"), 1512 TRANSLATE_STR("Achievements", 1513 "An unlock request could not be completed. We will keep retrying to submit this request."), 1514 Host::OSD_ERROR_DURATION); 1515 } 1516 } 1517 1518 void Achievements::HandleServerReconnectedEvent(const rc_client_event_t* event) 1519 { 1520 WARNING_LOG("Server reconnected."); 1521 1522 if (FullscreenUI::Initialize()) 1523 { 1524 ImGuiFullscreen::ShowToast(TRANSLATE_STR("Achievements", "Achievements Reconnected"), 1525 TRANSLATE_STR("Achievements", "All pending unlock requests have completed."), 1526 Host::OSD_INFO_DURATION); 1527 } 1528 } 1529 1530 void Achievements::ResetClient() 1531 { 1532 #ifdef ENABLE_RAINTEGRATION 1533 if (IsUsingRAIntegration()) 1534 { 1535 RA_OnReset(); 1536 return; 1537 } 1538 #endif 1539 1540 if (!IsActive()) 1541 return; 1542 1543 DEV_LOG("Reset client"); 1544 rc_client_reset(s_client); 1545 } 1546 1547 void Achievements::OnSystemPaused(bool paused) 1548 { 1549 #ifdef ENABLE_RAINTEGRATION 1550 if (IsUsingRAIntegration()) 1551 RA_SetPaused(paused); 1552 #endif 1553 } 1554 1555 void Achievements::DisableHardcoreMode() 1556 { 1557 if (!IsActive()) 1558 return; 1559 1560 #ifdef ENABLE_RAINTEGRATION 1561 if (IsUsingRAIntegration()) 1562 { 1563 if (RA_HardcoreModeIsActive()) 1564 RA_DisableHardcore(); 1565 1566 return; 1567 } 1568 #endif 1569 1570 if (!s_hardcore_mode) 1571 return; 1572 1573 SetHardcoreMode(false, true); 1574 } 1575 1576 bool Achievements::ResetHardcoreMode(bool is_booting) 1577 { 1578 if (!IsActive()) 1579 return false; 1580 1581 const auto lock = GetLock(); 1582 1583 // If we're not logged in, don't apply hardcore mode restrictions. 1584 // If we later log in, we'll start with it off anyway. 1585 const bool wanted_hardcore_mode = 1586 (IsLoggedInOrLoggingIn() || s_load_game_request) && g_settings.achievements_hardcore_mode; 1587 if (s_hardcore_mode == wanted_hardcore_mode) 1588 return false; 1589 1590 if (!is_booting && wanted_hardcore_mode && !CanEnableHardcoreMode()) 1591 return false; 1592 1593 SetHardcoreMode(wanted_hardcore_mode, false); 1594 return true; 1595 } 1596 1597 void Achievements::SetHardcoreMode(bool enabled, bool force_display_message) 1598 { 1599 if (enabled == s_hardcore_mode) 1600 return; 1601 1602 // new mode 1603 s_hardcore_mode = enabled; 1604 1605 if (System::IsValid() && (HasActiveGame() || force_display_message) && FullscreenUI::Initialize()) 1606 { 1607 ImGuiFullscreen::ShowToast(std::string(), 1608 enabled ? TRANSLATE_STR("Achievements", "Hardcore mode is now enabled.") : 1609 TRANSLATE_STR("Achievements", "Hardcore mode is now disabled."), 1610 Host::OSD_INFO_DURATION); 1611 } 1612 1613 rc_client_set_hardcore_enabled(s_client, enabled); 1614 DebugAssert((rc_client_get_hardcore_enabled(s_client) != 0) == enabled); 1615 if (HasActiveGame()) 1616 { 1617 UpdateGameSummary(); 1618 DisplayAchievementSummary(); 1619 } 1620 1621 // Reload setting to permit cheating-like things if we were just disabled. 1622 if (!enabled) 1623 Host::RunOnCPUThread([]() { System::ApplySettings(false); }); 1624 1625 // Toss away UI state, because it's invalid now 1626 ClearUIState(); 1627 1628 Host::OnAchievementsHardcoreModeChanged(enabled); 1629 } 1630 1631 bool Achievements::DoState(StateWrapper& sw) 1632 { 1633 // if we're inactive, we still need to skip the data (if any) 1634 if (!IsActive()) 1635 { 1636 u32 data_size = 0; 1637 sw.Do(&data_size); 1638 if (data_size > 0) 1639 sw.SkipBytes(data_size); 1640 1641 return !sw.HasError(); 1642 } 1643 1644 std::unique_lock lock(s_achievements_mutex); 1645 1646 if (sw.IsReading()) 1647 { 1648 // if we're active, make sure we've downloaded and activated all the achievements 1649 // before deserializing, otherwise that state's going to get lost. 1650 if (!IsUsingRAIntegration() && s_load_game_request) 1651 { 1652 Host::DisplayLoadingScreen("Downloading achievements data..."); 1653 s_http_downloader->WaitForAllRequests(); 1654 } 1655 1656 u32 data_size = 0; 1657 sw.Do(&data_size); 1658 if (data_size == 0) 1659 { 1660 // reset runtime, no data (state might've been created without cheevos) 1661 DEV_LOG("State is missing cheevos data, resetting runtime"); 1662 #ifdef ENABLE_RAINTEGRATION 1663 if (IsUsingRAIntegration()) 1664 RA_OnReset(); 1665 else 1666 rc_client_reset(s_client); 1667 #else 1668 rc_client_reset(s_client); 1669 #endif 1670 1671 return !sw.HasError(); 1672 } 1673 1674 if (data_size > s_state_buffer.size()) 1675 s_state_buffer.resize(data_size); 1676 if (data_size > 0) 1677 sw.DoBytes(s_state_buffer.data(), data_size); 1678 if (sw.HasError()) 1679 return false; 1680 1681 #ifdef ENABLE_RAINTEGRATION 1682 if (IsUsingRAIntegration()) 1683 { 1684 RA_RestoreState(reinterpret_cast<const char*>(s_state_buffer.data())); 1685 } 1686 else 1687 { 1688 const int result = rc_client_deserialize_progress_sized(s_client, s_state_buffer.data(), data_size); 1689 if (result != RC_OK) 1690 { 1691 WARNING_LOG("Failed to deserialize cheevos state ({}), resetting", result); 1692 rc_client_reset(s_client); 1693 } 1694 } 1695 #endif 1696 1697 return true; 1698 } 1699 else 1700 { 1701 size_t data_size; 1702 1703 #ifdef ENABLE_RAINTEGRATION 1704 if (IsUsingRAIntegration()) 1705 { 1706 const int size = RA_CaptureState(nullptr, 0); 1707 1708 data_size = (size >= 0) ? static_cast<u32>(size) : 0; 1709 s_state_buffer.resize(data_size); 1710 1711 if (data_size > 0) 1712 { 1713 const int result = RA_CaptureState(reinterpret_cast<char*>(s_state_buffer.data()), static_cast<int>(data_size)); 1714 if (result != static_cast<int>(data_size)) 1715 { 1716 WARNING_LOG("Failed to serialize cheevos state from RAIntegration."); 1717 data_size = 0; 1718 } 1719 } 1720 } 1721 else 1722 #endif 1723 { 1724 data_size = rc_client_progress_size(s_client); 1725 if (data_size > 0) 1726 { 1727 if (s_state_buffer.size() < data_size) 1728 s_state_buffer.resize(data_size); 1729 1730 const int result = rc_client_serialize_progress_sized(s_client, s_state_buffer.data(), data_size); 1731 if (result != RC_OK) 1732 { 1733 // set data to zero, effectively serializing nothing 1734 WARNING_LOG("Failed to serialize cheevos state ({})", result); 1735 data_size = 0; 1736 } 1737 } 1738 } 1739 1740 sw.Do(&data_size); 1741 if (data_size > 0) 1742 sw.DoBytes(s_state_buffer.data(), data_size); 1743 1744 return !sw.HasError(); 1745 } 1746 } 1747 1748 std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* achievement, int state, 1749 bool download_if_missing) 1750 { 1751 const std::string path = GetLocalImagePath(achievement->badge_name, (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? 1752 RC_IMAGE_TYPE_ACHIEVEMENT : 1753 RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED); 1754 if (download_if_missing && !path.empty() && !FileSystem::FileExists(path.c_str())) 1755 { 1756 char buf[512]; 1757 const int res = rc_client_achievement_get_image_url(achievement, state, buf, std::size(buf)); 1758 if (res == RC_OK) 1759 DownloadImage(buf, path); 1760 else 1761 ReportRCError(res, "rc_client_achievement_get_image_url() for {} failed", achievement->title); 1762 } 1763 1764 return path; 1765 } 1766 1767 std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry) 1768 { 1769 // TODO: maybe we should just cache these in memory... 1770 const std::string path = GetLocalImagePath(entry->user, RC_IMAGE_TYPE_USER); 1771 1772 if (!FileSystem::FileExists(path.c_str())) 1773 { 1774 char buf[512]; 1775 const int res = rc_client_leaderboard_entry_get_user_image_url(entry, buf, std::size(buf)); 1776 if (res == RC_OK) 1777 DownloadImage(buf, path); 1778 else 1779 ReportRCError(res, "rc_client_leaderboard_entry_get_user_image_url() for {} failed", entry->user); 1780 } 1781 1782 return path; 1783 } 1784 1785 bool Achievements::IsLoggedInOrLoggingIn() 1786 { 1787 return (rc_client_get_user_info(s_client) != nullptr || s_login_request); 1788 } 1789 1790 bool Achievements::CanEnableHardcoreMode() 1791 { 1792 return (s_load_game_request || s_has_achievements || s_has_leaderboards); 1793 } 1794 1795 bool Achievements::Login(const char* username, const char* password, Error* error) 1796 { 1797 auto lock = GetLock(); 1798 1799 // We need to use a temporary client if achievements aren't currently active. 1800 rc_client_t* client = s_client; 1801 HTTPDownloader* http = s_http_downloader.get(); 1802 const bool is_temporary_client = (client == nullptr); 1803 std::unique_ptr<HTTPDownloader> temporary_downloader; 1804 ScopedGuard temporary_client_guard = [&client, is_temporary_client, &temporary_downloader]() { 1805 if (is_temporary_client) 1806 DestroyClient(&client, &temporary_downloader); 1807 }; 1808 if (is_temporary_client) 1809 { 1810 if (!CreateClient(&client, &temporary_downloader)) 1811 { 1812 Error::SetString(error, "Failed to create client."); 1813 return false; 1814 } 1815 http = temporary_downloader.get(); 1816 } 1817 1818 LoginWithPasswordParameters params = {username, error, nullptr, false}; 1819 1820 params.request = 1821 rc_client_begin_login_with_password(client, username, password, ClientLoginWithPasswordCallback, ¶ms); 1822 if (!params.request) 1823 { 1824 Error::SetString(error, "Failed to create login request."); 1825 return false; 1826 } 1827 1828 // Wait until the login request completes. 1829 http->WaitForAllRequests(); 1830 Assert(!params.request); 1831 1832 // Success? Assume the callback set the error message. 1833 if (!params.result) 1834 return false; 1835 1836 // If we were't a temporary client, get the game loaded. 1837 if (System::IsValid() && !is_temporary_client) 1838 BeginLoadGame(); 1839 1840 return true; 1841 } 1842 1843 void Achievements::ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, 1844 void* userdata) 1845 { 1846 Assert(userdata); 1847 1848 LoginWithPasswordParameters* params = static_cast<LoginWithPasswordParameters*>(userdata); 1849 params->request = nullptr; 1850 1851 if (result != RC_OK) 1852 { 1853 ERROR_LOG("Login failed: {}: {}", rc_error_str(result), error_message ? error_message : "Unknown"); 1854 Error::SetString(params->error, 1855 fmt::format("{}: {}", rc_error_str(result), error_message ? error_message : "Unknown")); 1856 params->result = false; 1857 return; 1858 } 1859 1860 // Grab the token from the client, and save it to the config. 1861 const rc_client_user_t* user = rc_client_get_user_info(client); 1862 if (!user || !user->token) 1863 { 1864 ERROR_LOG("rc_client_get_user_info() returned NULL"); 1865 Error::SetString(params->error, "rc_client_get_user_info() returned NULL"); 1866 params->result = false; 1867 return; 1868 } 1869 1870 params->result = true; 1871 1872 // Store configuration. 1873 Host::SetBaseStringSettingValue("Cheevos", "Username", params->username); 1874 Host::SetBaseStringSettingValue("Cheevos", "Token", user->token); 1875 Host::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str()); 1876 Host::CommitBaseSettingChanges(); 1877 1878 ShowLoginSuccess(client); 1879 } 1880 1881 void Achievements::ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, 1882 void* userdata) 1883 { 1884 s_login_request = nullptr; 1885 1886 if (result != RC_OK) 1887 { 1888 ReportFmtError("Login failed: {}", error_message); 1889 Host::OnAchievementsLoginRequested(LoginRequestReason::TokenInvalid); 1890 return; 1891 } 1892 1893 ShowLoginSuccess(client); 1894 1895 if (System::IsValid()) 1896 BeginLoadGame(); 1897 } 1898 1899 void Achievements::ShowLoginSuccess(const rc_client_t* client) 1900 { 1901 const rc_client_user_t* user = rc_client_get_user_info(client); 1902 if (!user) 1903 return; 1904 1905 Host::OnAchievementsLoginSuccess(user->username, user->score, user->score_softcore, user->num_unread_messages); 1906 1907 if (System::IsValid()) 1908 { 1909 const auto lock = GetLock(); 1910 if (s_client == client) 1911 Host::RunOnCPUThread(ShowLoginNotification); 1912 } 1913 } 1914 1915 void Achievements::ShowLoginNotification() 1916 { 1917 const rc_client_user_t* user = rc_client_get_user_info(s_client); 1918 if (!user) 1919 return; 1920 1921 if (g_settings.achievements_notifications && FullscreenUI::Initialize()) 1922 { 1923 std::string badge_path = GetLoggedInUserBadgePath(); 1924 std::string title = user->display_name; 1925 1926 //: Summary for login notification. 1927 std::string summary = fmt::format(TRANSLATE_FS("Achievements", "Score: {} ({} softcore)\nUnread messages: {}"), 1928 user->score, user->score_softcore, user->num_unread_messages); 1929 1930 ImGuiFullscreen::AddNotification("achievements_login", LOGIN_NOTIFICATION_TIME, std::move(title), 1931 std::move(summary), std::move(badge_path)); 1932 } 1933 } 1934 1935 const char* Achievements::GetLoggedInUserName() 1936 { 1937 const rc_client_user_t* user = rc_client_get_user_info(s_client); 1938 if (!user) [[unlikely]] 1939 return nullptr; 1940 1941 return user->username; 1942 } 1943 1944 std::string Achievements::GetLoggedInUserBadgePath() 1945 { 1946 std::string badge_path; 1947 1948 const rc_client_user_t* user = rc_client_get_user_info(s_client); 1949 if (!user) [[unlikely]] 1950 return badge_path; 1951 1952 badge_path = GetLocalImagePath(user->username, RC_IMAGE_TYPE_USER); 1953 if (!badge_path.empty() && !FileSystem::FileExists(badge_path.c_str())) [[unlikely]] 1954 { 1955 char url[512]; 1956 const int res = rc_client_user_get_image_url(user, url, std::size(url)); 1957 if (res == RC_OK) 1958 DownloadImage(url, badge_path); 1959 else 1960 ReportRCError(res, "rc_client_user_get_image_url() failed: "); 1961 } 1962 1963 return badge_path; 1964 } 1965 1966 void Achievements::Logout() 1967 { 1968 if (IsActive()) 1969 { 1970 const auto lock = GetLock(); 1971 1972 if (HasActiveGame()) 1973 { 1974 ClearGameInfo(); 1975 UpdateGlyphRanges(); 1976 } 1977 1978 INFO_LOG("Logging out..."); 1979 rc_client_logout(s_client); 1980 } 1981 1982 INFO_LOG("Clearing credentials..."); 1983 Host::DeleteBaseSettingValue("Cheevos", "Username"); 1984 Host::DeleteBaseSettingValue("Cheevos", "Token"); 1985 Host::DeleteBaseSettingValue("Cheevos", "LoginTimestamp"); 1986 Host::CommitBaseSettingChanges(); 1987 } 1988 1989 bool Achievements::ConfirmSystemReset() 1990 { 1991 #ifdef ENABLE_RAINTEGRATION 1992 if (IsUsingRAIntegration()) 1993 return RA_ConfirmLoadNewRom(false); 1994 #endif 1995 1996 return true; 1997 } 1998 1999 bool Achievements::ConfirmHardcoreModeDisable(const char* trigger) 2000 { 2001 #ifdef ENABLE_RAINTEGRATION 2002 if (IsUsingRAIntegration()) 2003 return (RA_WarnDisableHardcore(trigger) != 0); 2004 #endif 2005 2006 // I really hope this doesn't deadlock :/ 2007 const bool confirmed = Host::ConfirmMessage( 2008 TRANSLATE("Achievements", "Confirm Hardcore Mode"), 2009 fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you " 2010 "want to disable hardcore mode? {0} will be cancelled if you select No."), 2011 trigger)); 2012 if (!confirmed) 2013 return false; 2014 2015 DisableHardcoreMode(); 2016 return true; 2017 } 2018 2019 void Achievements::ConfirmHardcoreModeDisableAsync(const char* trigger, std::function<void(bool)> callback) 2020 { 2021 #ifndef __ANDROID__ 2022 #ifdef ENABLE_RAINTEGRATION 2023 if (IsUsingRAIntegration()) 2024 { 2025 const bool result = (RA_WarnDisableHardcore(trigger) != 0); 2026 callback(result); 2027 return; 2028 } 2029 #endif 2030 2031 if (!FullscreenUI::Initialize()) 2032 { 2033 Host::AddOSDMessage(fmt::format(TRANSLATE_FS("Achievements", "Cannot {} while hardcode mode is active."), trigger), 2034 Host::OSD_WARNING_DURATION); 2035 callback(false); 2036 return; 2037 } 2038 2039 auto real_callback = [callback = std::move(callback)](bool res) mutable { 2040 // don't run the callback in the middle of rendering the UI 2041 Host::RunOnCPUThread([callback = std::move(callback), res]() { 2042 if (res) 2043 DisableHardcoreMode(); 2044 callback(res); 2045 }); 2046 }; 2047 2048 ImGuiFullscreen::OpenConfirmMessageDialog( 2049 TRANSLATE_STR("Achievements", "Confirm Hardcore Mode"), 2050 fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you " 2051 "want to disable hardcore mode? {0} will be cancelled if you select No."), 2052 trigger), 2053 std::move(real_callback), fmt::format(ICON_FA_CHECK " {}", TRANSLATE_SV("Achievements", "Yes")), 2054 fmt::format(ICON_FA_TIMES " {}", TRANSLATE_SV("Achievements", "No"))); 2055 #else 2056 Host::AddOSDMessage(fmt::format(TRANSLATE_FS("Achievements", "Cannot {} while hardcode mode is active."), trigger), 2057 Host::OSD_WARNING_DURATION); 2058 callback(false); 2059 #endif 2060 } 2061 2062 void Achievements::ClearUIState() 2063 { 2064 #ifndef __ANDROID__ 2065 if (FullscreenUI::IsAchievementsWindowOpen() || FullscreenUI::IsLeaderboardsWindowOpen()) 2066 FullscreenUI::ReturnToPreviousWindow(); 2067 2068 CloseLeaderboard(); 2069 #endif 2070 2071 s_achievement_badge_paths = {}; 2072 2073 s_leaderboard_user_icon_paths = {}; 2074 s_leaderboard_entry_lists = {}; 2075 if (s_leaderboard_list) 2076 { 2077 rc_client_destroy_leaderboard_list(s_leaderboard_list); 2078 s_leaderboard_list = nullptr; 2079 } 2080 2081 if (s_achievement_list) 2082 { 2083 rc_client_destroy_achievement_list(s_achievement_list); 2084 s_achievement_list = nullptr; 2085 } 2086 } 2087 2088 template<typename T> 2089 static float IndicatorOpacity(const T& i) 2090 { 2091 const float elapsed = static_cast<float>(i.show_hide_time.GetTimeSeconds()); 2092 const float time = i.active ? Achievements::INDICATOR_FADE_IN_TIME : Achievements::INDICATOR_FADE_OUT_TIME; 2093 const float opacity = (elapsed >= time) ? 1.0f : (elapsed / time); 2094 return (i.active) ? opacity : (1.0f - opacity); 2095 } 2096 2097 void Achievements::DrawGameOverlays() 2098 { 2099 using ImGuiFullscreen::g_medium_font; 2100 using ImGuiFullscreen::LayoutScale; 2101 2102 if (!HasActiveGame() || !g_settings.achievements_overlays) 2103 return; 2104 2105 const auto lock = GetLock(); 2106 2107 const float spacing = LayoutScale(10.0f); 2108 const float padding = LayoutScale(10.0f); 2109 const ImVec2 image_size = 2110 LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT); 2111 const ImGuiIO& io = ImGui::GetIO(); 2112 ImVec2 position = ImVec2(io.DisplaySize.x - padding, io.DisplaySize.y - padding); 2113 ImDrawList* dl = ImGui::GetBackgroundDrawList(); 2114 2115 if (!s_active_challenge_indicators.empty()) 2116 { 2117 const float x_advance = image_size.x + spacing; 2118 ImVec2 current_position = ImVec2(position.x - image_size.x, position.y - image_size.y); 2119 2120 for (auto it = s_active_challenge_indicators.begin(); it != s_active_challenge_indicators.end();) 2121 { 2122 const AchievementChallengeIndicator& indicator = *it; 2123 const float opacity = IndicatorOpacity(indicator); 2124 const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); 2125 2126 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); 2127 if (badge) 2128 { 2129 dl->AddImage(badge, current_position, current_position + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), 2130 col); 2131 current_position.x -= x_advance; 2132 } 2133 2134 if (!indicator.active && opacity <= 0.01f) 2135 { 2136 DEV_LOG("Remove challenge indicator"); 2137 it = s_active_challenge_indicators.erase(it); 2138 } 2139 else 2140 { 2141 ++it; 2142 } 2143 } 2144 2145 position.y -= image_size.y + padding; 2146 } 2147 2148 if (s_active_progress_indicator.has_value()) 2149 { 2150 const AchievementProgressIndicator& indicator = s_active_progress_indicator.value(); 2151 const float opacity = IndicatorOpacity(indicator); 2152 const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); 2153 2154 const char* text_start = s_active_progress_indicator->achievement->measured_progress; 2155 const char* text_end = text_start + std::strlen(text_start); 2156 const ImVec2 text_size = g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text_start, text_end); 2157 2158 const ImVec2 box_min = ImVec2(position.x - image_size.x - text_size.x - spacing - padding * 2.0f, 2159 position.y - image_size.y - padding * 2.0f); 2160 const ImVec2 box_max = position; 2161 const float box_rounding = LayoutScale(1.0f); 2162 2163 dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)), box_rounding); 2164 dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding); 2165 2166 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); 2167 if (badge) 2168 { 2169 const ImVec2 badge_pos = box_min + ImVec2(padding, padding); 2170 dl->AddImage(badge, badge_pos, badge_pos + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), col); 2171 } 2172 2173 const ImVec2 text_pos = 2174 box_min + ImVec2(padding + image_size.x + spacing, (box_max.y - box_min.y - text_size.y) * 0.5f); 2175 const ImVec4 text_clip_rect(text_pos.x, text_pos.y, box_max.x, box_max.y); 2176 dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, col, text_start, text_end, 0.0f, &text_clip_rect); 2177 2178 if (!indicator.active && opacity <= 0.01f) 2179 { 2180 DEV_LOG("Remove progress indicator"); 2181 s_active_progress_indicator.reset(); 2182 } 2183 2184 position.y -= image_size.y - padding * 3.0f; 2185 } 2186 2187 if (!s_active_leaderboard_trackers.empty()) 2188 { 2189 for (auto it = s_active_leaderboard_trackers.begin(); it != s_active_leaderboard_trackers.end();) 2190 { 2191 const LeaderboardTrackerIndicator& indicator = *it; 2192 const float opacity = IndicatorOpacity(indicator); 2193 2194 TinyString width_string; 2195 width_string.append(ICON_FA_STOPWATCH); 2196 for (u32 i = 0; i < indicator.text.length(); i++) 2197 width_string.append('0'); 2198 const ImVec2 size = ImGuiFullscreen::g_medium_font->CalcTextSizeA( 2199 ImGuiFullscreen::g_medium_font->FontSize, FLT_MAX, 0.0f, width_string.c_str(), width_string.end_ptr()); 2200 2201 const ImVec2 box_min = ImVec2(position.x - size.x - padding * 2.0f, position.y - size.y - padding * 2.0f); 2202 const ImVec2 box_max = position; 2203 const float box_rounding = LayoutScale(1.0f); 2204 dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)), 2205 box_rounding); 2206 dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding); 2207 2208 const u32 text_col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); 2209 const ImVec2 text_size = ImGuiFullscreen::g_medium_font->CalcTextSizeA( 2210 ImGuiFullscreen::g_medium_font->FontSize, FLT_MAX, 0.0f, indicator.text.c_str(), 2211 indicator.text.c_str() + indicator.text.length()); 2212 const ImVec2 text_pos = ImVec2(box_max.x - padding - text_size.x, box_min.y + padding); 2213 const ImVec4 text_clip_rect(box_min.x, box_min.y, box_max.x, box_max.y); 2214 dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, text_col, indicator.text.c_str(), 2215 indicator.text.c_str() + indicator.text.length(), 0.0f, &text_clip_rect); 2216 2217 const ImVec2 icon_pos = ImVec2(box_min.x + padding, box_min.y + padding); 2218 dl->AddText(g_medium_font, g_medium_font->FontSize, icon_pos, text_col, ICON_FA_STOPWATCH, nullptr, 0.0f, 2219 &text_clip_rect); 2220 2221 if (!indicator.active && opacity <= 0.01f) 2222 { 2223 DEV_LOG("Remove tracker indicator"); 2224 it = s_active_leaderboard_trackers.erase(it); 2225 } 2226 else 2227 { 2228 ++it; 2229 } 2230 2231 position.x = box_min.x - padding; 2232 } 2233 2234 // Uncomment if there are any other overlays above this one. 2235 // position.y -= image_size.y - padding * 3.0f; 2236 } 2237 } 2238 2239 #ifndef __ANDROID__ 2240 2241 void Achievements::DrawPauseMenuOverlays() 2242 { 2243 using ImGuiFullscreen::g_large_font; 2244 using ImGuiFullscreen::g_medium_font; 2245 using ImGuiFullscreen::LayoutScale; 2246 2247 if (!HasActiveGame()) 2248 return; 2249 2250 const auto lock = GetLock(); 2251 2252 if (s_active_challenge_indicators.empty() && !s_active_progress_indicator.has_value()) 2253 return; 2254 2255 const ImGuiIO& io = ImGui::GetIO(); 2256 ImFont* font = g_medium_font; 2257 2258 const ImVec2 image_size(LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, 2259 ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)); 2260 const float start_y = LayoutScale(10.0f + 4.0f + 4.0f) + g_large_font->FontSize + (g_medium_font->FontSize * 2.0f); 2261 const float margin = LayoutScale(10.0f); 2262 const float spacing = LayoutScale(10.0f); 2263 const float padding = LayoutScale(10.0f); 2264 2265 const float max_text_width = ImGuiFullscreen::LayoutScale(300.0f); 2266 const float row_width = max_text_width + padding + padding + image_size.x + spacing; 2267 const float title_height = padding + font->FontSize + padding; 2268 2269 if (!s_active_challenge_indicators.empty()) 2270 { 2271 const ImVec2 box_min(io.DisplaySize.x - row_width - margin, start_y + margin); 2272 const ImVec2 box_max(box_min.x + row_width, 2273 box_min.y + title_height + 2274 (static_cast<float>(s_active_challenge_indicators.size()) * (image_size.y + padding))); 2275 2276 ImDrawList* dl = ImGui::GetBackgroundDrawList(); 2277 dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f)); 2278 dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255), 2279 TRANSLATE("Achievements", "Active Challenge Achievements")); 2280 2281 const float y_advance = image_size.y + spacing; 2282 const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f; 2283 const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f); 2284 ImVec2 position(box_min.x + padding, box_min.y + title_height); 2285 2286 for (const AchievementChallengeIndicator& indicator : s_active_challenge_indicators) 2287 { 2288 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); 2289 if (!badge) 2290 continue; 2291 2292 dl->AddImage(badge, position, position + image_size); 2293 2294 const char* achievement_title = indicator.achievement->title; 2295 const char* achievement_title_end = achievement_title + std::strlen(indicator.achievement->title); 2296 const char* remaining_text = nullptr; 2297 const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title, 2298 achievement_title_end, &remaining_text)); 2299 const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset); 2300 const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width, 2301 text_position.y + image_size.y); 2302 const u32 text_color = IM_COL32(255, 255, 255, 255); 2303 2304 if (remaining_text < achievement_title_end) 2305 { 2306 dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f, 2307 &text_bbox); 2308 dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...", 2309 nullptr, 0.0f, &text_bbox); 2310 } 2311 else 2312 { 2313 dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, achievement_title_end, 0.0f, 2314 &text_bbox); 2315 } 2316 2317 position.y += y_advance; 2318 } 2319 } 2320 } 2321 2322 bool Achievements::PrepareAchievementsWindow() 2323 { 2324 auto lock = Achievements::GetLock(); 2325 2326 s_achievement_badge_paths = {}; 2327 2328 if (s_achievement_list) 2329 rc_client_destroy_achievement_list(s_achievement_list); 2330 s_achievement_list = rc_client_create_achievement_list( 2331 s_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, 2332 RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS /*RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE*/); 2333 if (!s_achievement_list) 2334 { 2335 ERROR_LOG("rc_client_create_achievement_list() returned null"); 2336 return false; 2337 } 2338 2339 return true; 2340 } 2341 2342 void Achievements::DrawAchievementsWindow() 2343 { 2344 using ImGuiFullscreen::g_large_font; 2345 using ImGuiFullscreen::g_medium_font; 2346 using ImGuiFullscreen::LayoutScale; 2347 2348 if (!s_achievement_list) 2349 return; 2350 2351 auto lock = Achievements::GetLock(); 2352 2353 static constexpr float alpha = 0.8f; 2354 static constexpr float heading_alpha = 0.95f; 2355 static constexpr float heading_height_unscaled = 110.0f; 2356 2357 const ImVec4 background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, alpha); 2358 const ImVec4 heading_background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, heading_alpha); 2359 const ImVec2 display_size = ImGui::GetIO().DisplaySize; 2360 const float heading_height = ImGuiFullscreen::LayoutScale(heading_height_unscaled); 2361 bool close_window = false; 2362 2363 if (ImGuiFullscreen::BeginFullscreenWindow( 2364 ImVec2(), ImVec2(display_size.x, heading_height), "achievements_heading", heading_background, 0.0f, ImVec2(), 2365 ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) 2366 { 2367 ImRect bb; 2368 bool visible, hovered; 2369 ImGuiFullscreen::MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered, 2370 &bb.Min, &bb.Max, 0, heading_alpha); 2371 if (visible) 2372 { 2373 const float padding = ImGuiFullscreen::LayoutScale(10.0f); 2374 const float spacing = ImGuiFullscreen::LayoutScale(10.0f); 2375 const float image_height = ImGuiFullscreen::LayoutScale(85.0f); 2376 2377 const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); 2378 const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); 2379 2380 if (!s_game_icon.empty()) 2381 { 2382 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str()); 2383 if (badge) 2384 { 2385 ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), 2386 IM_COL32(255, 255, 255, 255)); 2387 } 2388 } 2389 2390 float left = bb.Min.x + padding + image_height + spacing; 2391 float right = bb.Max.x - padding; 2392 float top = bb.Min.y + padding; 2393 ImDrawList* dl = ImGui::GetWindowDrawList(); 2394 SmallString text; 2395 ImVec2 text_size; 2396 2397 close_window = (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, 2398 true, g_large_font) || 2399 ImGuiFullscreen::WantsToCloseMenu()); 2400 2401 const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); 2402 text.assign(s_game_title); 2403 2404 if (s_hardcore_mode) 2405 text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)")); 2406 2407 top += g_large_font->FontSize + spacing; 2408 2409 ImGui::PushFont(g_large_font); 2410 ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f), 2411 &title_bb); 2412 ImGui::PopFont(); 2413 2414 const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); 2415 if (s_game_summary.num_core_achievements > 0) 2416 { 2417 if (s_game_summary.num_unlocked_achievements == s_game_summary.num_core_achievements) 2418 { 2419 text = TRANSLATE_PLURAL_SSTR("Achievements", "You have unlocked all achievements and earned {} points!", 2420 "Point count", s_game_summary.points_unlocked); 2421 } 2422 else 2423 { 2424 text.format(TRANSLATE_FS("Achievements", 2425 "You have unlocked {0} of {1} achievements, earning {2} of {3} possible points."), 2426 s_game_summary.num_unlocked_achievements, s_game_summary.num_core_achievements, 2427 s_game_summary.points_unlocked, s_game_summary.points_core); 2428 } 2429 } 2430 else 2431 { 2432 text.assign(TRANSLATE_SV("Achievements", "This game has no achievements.")); 2433 } 2434 2435 top += g_medium_font->FontSize + spacing; 2436 2437 ImGui::PushFont(g_medium_font); 2438 ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.c_str(), text.end_ptr(), nullptr, 2439 ImVec2(0.0f, 0.0f), &summary_bb); 2440 ImGui::PopFont(); 2441 2442 if (s_game_summary.num_core_achievements > 0) 2443 { 2444 const float progress_height = ImGuiFullscreen::LayoutScale(20.0f); 2445 const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height)); 2446 const float fraction = static_cast<float>(s_game_summary.num_unlocked_achievements) / 2447 static_cast<float>(s_game_summary.num_core_achievements); 2448 dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); 2449 dl->AddRectFilled(progress_bb.Min, 2450 ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), 2451 ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); 2452 2453 text.format("{}%", static_cast<int>(std::round(fraction * 100.0f))); 2454 text_size = ImGui::CalcTextSize(text.c_str(), text.end_ptr()); 2455 const ImVec2 text_pos( 2456 progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), 2457 progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)); 2458 dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, 2459 ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), text.c_str(), text.end_ptr()); 2460 top += progress_height + spacing; 2461 } 2462 } 2463 } 2464 ImGuiFullscreen::EndFullscreenWindow(); 2465 2466 ImGui::SetNextWindowBgAlpha(alpha); 2467 2468 // See note in FullscreenUI::DrawSettingsWindow(). 2469 if (ImGuiFullscreen::IsFocusResetFromWindowChange()) 2470 ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); 2471 2472 if (ImGuiFullscreen::BeginFullscreenWindow( 2473 ImVec2(0.0f, heading_height), 2474 ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)), 2475 "achievements", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0)) 2476 { 2477 static bool buckets_collapsed[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS] = {}; 2478 static const char* bucket_names[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS] = { 2479 TRANSLATE_NOOP("Achievements", "Unknown"), TRANSLATE_NOOP("Achievements", "Locked"), 2480 TRANSLATE_NOOP("Achievements", "Unlocked"), TRANSLATE_NOOP("Achievements", "Unsupported"), 2481 TRANSLATE_NOOP("Achievements", "Unofficial"), TRANSLATE_NOOP("Achievements", "Recently Unlocked"), 2482 TRANSLATE_NOOP("Achievements", "Active Challenges"), TRANSLATE_NOOP("Achievements", "Almost There"), 2483 }; 2484 2485 ImGuiFullscreen::ResetFocusHere(); 2486 ImGuiFullscreen::BeginMenuButtons(); 2487 2488 for (u32 bucket_type : {RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, 2489 RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED, 2490 RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, 2491 RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED}) 2492 { 2493 for (u32 bucket_idx = 0; bucket_idx < s_achievement_list->num_buckets; bucket_idx++) 2494 { 2495 const rc_client_achievement_bucket_t& bucket = s_achievement_list->buckets[bucket_idx]; 2496 if (bucket.bucket_type != bucket_type) 2497 continue; 2498 2499 DebugAssert(bucket.bucket_type < NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS); 2500 2501 // TODO: Once subsets are supported, this will need to change. 2502 bool& bucket_collapsed = buckets_collapsed[bucket.bucket_type]; 2503 bucket_collapsed ^= 2504 ImGuiFullscreen::MenuHeadingButton(Host::TranslateToCString("Achievements", bucket_names[bucket.bucket_type]), 2505 bucket_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP); 2506 if (!bucket_collapsed) 2507 { 2508 for (u32 i = 0; i < bucket.num_achievements; i++) 2509 DrawAchievement(bucket.achievements[i]); 2510 } 2511 } 2512 } 2513 2514 ImGuiFullscreen::EndMenuButtons(); 2515 } 2516 ImGuiFullscreen::EndFullscreenWindow(); 2517 2518 FullscreenUI::SetStandardSelectionFooterText(true); 2519 2520 if (close_window) 2521 FullscreenUI::ReturnToPreviousWindow(); 2522 } 2523 2524 void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo) 2525 { 2526 using ImGuiFullscreen::g_large_font; 2527 using ImGuiFullscreen::g_medium_font; 2528 using ImGuiFullscreen::LayoutScale; 2529 using ImGuiFullscreen::LayoutUnscale; 2530 2531 static constexpr float alpha = 0.8f; 2532 static constexpr float progress_height_unscaled = 20.0f; 2533 static constexpr float progress_spacing_unscaled = 5.0f; 2534 2535 const float spacing = ImGuiFullscreen::LayoutScale(4.0f); 2536 2537 const bool is_unlocked = (cheevo->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); 2538 const std::string_view measured_progress(cheevo->measured_progress); 2539 const bool is_measured = !is_unlocked && !measured_progress.empty(); 2540 const float unlock_size = is_unlocked ? (spacing + ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE) : 0.0f; 2541 const ImVec2 points_template_size( 2542 g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, TRANSLATE("Achievements", "XXX points"))); 2543 2544 const size_t summary_length = std::strlen(cheevo->description); 2545 const float summary_wrap_width = 2546 (ImGui::GetCurrentWindow()->WorkRect.GetWidth() - (ImGui::GetStyle().FramePadding.x * 2.0f) - 2547 LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + 30.0f) - points_template_size.x); 2548 const ImVec2 summary_text_size(g_medium_font->CalcTextSizeA( 2549 g_medium_font->FontSize, FLT_MAX, summary_wrap_width, cheevo->description, cheevo->description + summary_length)); 2550 2551 // Messy, but need to undo LayoutScale in MenuButtonFrame()... 2552 const float extra_summary_height = LayoutUnscale(std::max(summary_text_size.y - g_medium_font->FontSize, 0.0f)); 2553 2554 ImRect bb; 2555 bool visible, hovered; 2556 const bool clicked = ImGuiFullscreen::MenuButtonFrame( 2557 TinyString::from_format("chv_{}", cheevo->id), true, 2558 !is_measured ? ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + extra_summary_height + unlock_size : 2559 ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + extra_summary_height + progress_height_unscaled + 2560 progress_spacing_unscaled, 2561 &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); 2562 if (!visible) 2563 return; 2564 2565 std::string* badge_path; 2566 if (const auto badge_it = std::find_if(s_achievement_badge_paths.begin(), s_achievement_badge_paths.end(), 2567 [cheevo](const auto& it) { return (it.first == cheevo); }); 2568 badge_it != s_achievement_badge_paths.end()) 2569 { 2570 badge_path = &badge_it->second; 2571 } 2572 else 2573 { 2574 std::string new_badge_path = Achievements::GetAchievementBadgePath(cheevo, cheevo->state); 2575 badge_path = &s_achievement_badge_paths.emplace_back(cheevo, std::move(new_badge_path)).second; 2576 } 2577 2578 const ImVec2 image_size( 2579 LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT)); 2580 if (!badge_path->empty()) 2581 { 2582 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(*badge_path); 2583 if (badge) 2584 { 2585 ImGui::GetWindowDrawList()->AddImage(badge, bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), 2586 IM_COL32(255, 255, 255, 255)); 2587 } 2588 } 2589 2590 SmallString text; 2591 2592 const float midpoint = bb.Min.y + g_large_font->FontSize + spacing; 2593 text = TRANSLATE_PLURAL_SSTR("Achievements", "%n points", "Achievement points", cheevo->points); 2594 const ImVec2 points_size( 2595 g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr())); 2596 const float points_template_start = bb.Max.x - points_template_size.x; 2597 const float points_start = points_template_start + ((points_template_size.x - points_size.x) * 0.5f); 2598 2599 const char* right_icon_text; 2600 switch (cheevo->type) 2601 { 2602 case RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE: 2603 right_icon_text = ICON_PF_ACHIEVEMENTS_MISSABLE; // Missable 2604 break; 2605 2606 case RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION: 2607 right_icon_text = ICON_PF_ACHIEVEMENTS_PROGRESSION; // Progression 2608 break; 2609 2610 case RC_CLIENT_ACHIEVEMENT_TYPE_WIN: 2611 right_icon_text = ICON_PF_ACHIEVEMENTS_WIN; // Win Condition 2612 break; 2613 2614 // Just use the lock for standard achievements. 2615 case RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD: 2616 default: 2617 right_icon_text = is_unlocked ? ICON_EMOJI_UNLOCKED : ICON_FA_LOCK; 2618 break; 2619 } 2620 2621 const ImVec2 right_icon_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, FLT_MAX, 0.0f, right_icon_text)); 2622 2623 const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); 2624 const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(points_start, midpoint)); 2625 const ImRect summary_bb(ImVec2(text_start_x, midpoint), 2626 ImVec2(points_start, midpoint + g_medium_font->FontSize + extra_summary_height)); 2627 const ImRect points_bb(ImVec2(points_start, midpoint), bb.Max); 2628 const ImRect lock_bb(ImVec2(points_template_start + ((points_template_size.x - right_icon_size.x) * 0.5f), bb.Min.y), 2629 ImVec2(bb.Max.x, midpoint)); 2630 2631 ImGui::PushFont(g_large_font); 2632 ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); 2633 ImGui::RenderTextClipped(lock_bb.Min, lock_bb.Max, right_icon_text, nullptr, &right_icon_size, ImVec2(0.0f, 0.0f), 2634 &lock_bb); 2635 ImGui::PopFont(); 2636 2637 ImGui::PushFont(g_medium_font); 2638 if (cheevo->description && summary_length > 0) 2639 { 2640 ImGui::RenderTextWrapped(summary_bb.Min, cheevo->description, cheevo->description + summary_length, 2641 summary_wrap_width); 2642 } 2643 ImGui::RenderTextClipped(points_bb.Min, points_bb.Max, text.c_str(), text.end_ptr(), &points_size, ImVec2(0.0f, 0.0f), 2644 &points_bb); 2645 2646 if (is_unlocked) 2647 { 2648 TinyString date; 2649 FullscreenUI::TimeToPrintableString(&date, cheevo->unlock_time); 2650 text.format(TRANSLATE_FS("Achievements", "Unlocked: {}"), date); 2651 2652 const ImRect unlock_bb(summary_bb.Min.x, summary_bb.Max.y + spacing, summary_bb.Max.x, bb.Max.y); 2653 ImGui::RenderTextClipped(unlock_bb.Min, unlock_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f), 2654 &unlock_bb); 2655 } 2656 else if (is_measured) 2657 { 2658 ImDrawList* dl = ImGui::GetWindowDrawList(); 2659 const float progress_height = LayoutScale(progress_height_unscaled); 2660 const float progress_spacing = LayoutScale(progress_spacing_unscaled); 2661 const float top = midpoint + g_medium_font->FontSize + progress_spacing; 2662 const ImRect progress_bb(ImVec2(text_start_x, top), ImVec2(bb.Max.x, top + progress_height)); 2663 const float fraction = cheevo->measured_percent * 0.01f; 2664 dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); 2665 dl->AddRectFilled(progress_bb.Min, ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), 2666 ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); 2667 2668 const ImVec2 text_size = 2669 ImGui::CalcTextSize(measured_progress.data(), measured_progress.data() + measured_progress.size()); 2670 const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), 2671 progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)); 2672 dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, 2673 ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), measured_progress.data(), 2674 measured_progress.data() + measured_progress.size()); 2675 } 2676 2677 if (clicked) 2678 { 2679 const SmallString url = SmallString::from_format(fmt::runtime(ACHEIVEMENT_DETAILS_URL_TEMPLATE), cheevo->id); 2680 INFO_LOG("Opening achievement details: {}", url); 2681 Host::OpenURL(url); 2682 } 2683 2684 ImGui::PopFont(); 2685 } 2686 2687 bool Achievements::PrepareLeaderboardsWindow() 2688 { 2689 auto lock = Achievements::GetLock(); 2690 rc_client_t* const client = s_client; 2691 2692 s_achievement_badge_paths = {}; 2693 CloseLeaderboard(); 2694 if (s_leaderboard_list) 2695 rc_client_destroy_leaderboard_list(s_leaderboard_list); 2696 s_leaderboard_list = rc_client_create_leaderboard_list(client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); 2697 if (!s_leaderboard_list) 2698 { 2699 ERROR_LOG("rc_client_create_leaderboard_list() returned null"); 2700 return false; 2701 } 2702 2703 return true; 2704 } 2705 2706 void Achievements::DrawLeaderboardsWindow() 2707 { 2708 using ImGuiFullscreen::g_large_font; 2709 using ImGuiFullscreen::g_medium_font; 2710 using ImGuiFullscreen::LayoutScale; 2711 2712 static constexpr float alpha = 0.8f; 2713 static constexpr float heading_alpha = 0.95f; 2714 static constexpr float heading_height_unscaled = 110.0f; 2715 static constexpr float tab_height_unscaled = 50.0f; 2716 2717 auto lock = Achievements::GetLock(); 2718 2719 const bool is_leaderboard_open = (s_open_leaderboard != nullptr); 2720 bool close_leaderboard_on_exit = false; 2721 2722 ImRect bb; 2723 2724 const ImVec4 background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, alpha); 2725 const ImVec4 heading_background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, heading_alpha); 2726 const ImVec2 display_size = ImGui::GetIO().DisplaySize; 2727 const float padding = LayoutScale(10.0f); 2728 const float spacing = LayoutScale(10.0f); 2729 const float spacing_small = spacing / 2.0f; 2730 float heading_height = LayoutScale(heading_height_unscaled); 2731 if (is_leaderboard_open) 2732 { 2733 // tabs 2734 heading_height += spacing_small + LayoutScale(tab_height_unscaled) + spacing; 2735 2736 // Add space for a legend - spacing + 1 line of text + spacing + line 2737 heading_height += LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing; 2738 } 2739 2740 const float rank_column_width = 2741 g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "99999").x; 2742 const float name_column_width = 2743 g_large_font 2744 ->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWWWWWWWWWWWWW") 2745 .x; 2746 const float time_column_width = 2747 g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWW").x; 2748 const float column_spacing = spacing * 2.0f; 2749 2750 if (ImGuiFullscreen::BeginFullscreenWindow( 2751 ImVec2(), ImVec2(display_size.x, heading_height), "leaderboards_heading", heading_background, 0.0f, ImVec2(), 2752 ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) 2753 { 2754 bool visible, hovered; 2755 bool pressed = ImGuiFullscreen::MenuButtonFrame("leaderboards_heading", false, heading_height_unscaled, &visible, 2756 &hovered, &bb.Min, &bb.Max, 0, alpha); 2757 UNREFERENCED_VARIABLE(pressed); 2758 2759 if (visible) 2760 { 2761 const float image_height = LayoutScale(85.0f); 2762 2763 const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); 2764 const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); 2765 2766 if (!s_game_icon.empty()) 2767 { 2768 GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str()); 2769 if (badge) 2770 { 2771 ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), 2772 IM_COL32(255, 255, 255, 255)); 2773 } 2774 } 2775 2776 float left = bb.Min.x + padding + image_height + spacing; 2777 float right = bb.Max.x - padding; 2778 float top = bb.Min.y + padding; 2779 SmallString text; 2780 2781 if (!is_leaderboard_open) 2782 { 2783 if (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, 2784 g_large_font) || 2785 ImGuiFullscreen::WantsToCloseMenu()) 2786 { 2787 FullscreenUI::ReturnToPreviousWindow(); 2788 } 2789 } 2790 else 2791 { 2792 if (ImGuiFullscreen::FloatingButton(ICON_FA_CARET_SQUARE_LEFT, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, 2793 g_large_font) || 2794 ImGuiFullscreen::WantsToCloseMenu()) 2795 { 2796 close_leaderboard_on_exit = true; 2797 } 2798 } 2799 2800 const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); 2801 text.assign(Achievements::GetGameTitle()); 2802 2803 top += g_large_font->FontSize + spacing; 2804 2805 ImGui::PushFont(g_large_font); 2806 ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f), 2807 &title_bb); 2808 ImGui::PopFont(); 2809 2810 if (is_leaderboard_open) 2811 { 2812 const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); 2813 text.assign(s_open_leaderboard->title); 2814 2815 top += g_large_font->FontSize + spacing_small; 2816 2817 ImGui::PushFont(g_large_font); 2818 ImGui::RenderTextClipped(subtitle_bb.Min, subtitle_bb.Max, text.c_str(), text.end_ptr(), nullptr, 2819 ImVec2(0.0f, 0.0f), &subtitle_bb); 2820 ImGui::PopFont(); 2821 2822 text.assign(s_open_leaderboard->description); 2823 } 2824 else 2825 { 2826 u32 count = 0; 2827 for (u32 i = 0; i < s_leaderboard_list->num_buckets; i++) 2828 count += s_leaderboard_list->buckets[i].num_leaderboards; 2829 text = TRANSLATE_PLURAL_SSTR("Achievements", "This game has %n leaderboards.", "Leaderboard count", count); 2830 } 2831 2832 const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); 2833 top += g_medium_font->FontSize + spacing_small; 2834 2835 ImGui::PushFont(g_medium_font); 2836 ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.c_str(), text.end_ptr(), nullptr, 2837 ImVec2(0.0f, 0.0f), &summary_bb); 2838 2839 if (!is_leaderboard_open && !Achievements::IsHardcoreModeActive()) 2840 { 2841 const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); 2842 top += g_medium_font->FontSize + spacing_small; 2843 2844 ImGui::RenderTextClipped( 2845 hardcore_warning_bb.Min, hardcore_warning_bb.Max, 2846 TRANSLATE("Achievements", 2847 "Submitting scores is disabled because hardcore mode is off. Leaderboards are read-only."), 2848 nullptr, nullptr, ImVec2(0.0f, 0.0f), &hardcore_warning_bb); 2849 } 2850 2851 ImGui::PopFont(); 2852 2853 if (is_leaderboard_open) 2854 { 2855 const float tab_width = (ImGui::GetWindowWidth() / ImGuiFullscreen::g_layout_scale) * 0.5f; 2856 ImGui::SetCursorPos(ImVec2(0.0f, top + spacing_small)); 2857 2858 if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, false) || 2859 ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, false) || 2860 ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, false) || 2861 ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, false) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, false)) 2862 { 2863 s_is_showing_all_leaderboard_entries = !s_is_showing_all_leaderboard_entries; 2864 ImGuiFullscreen::QueueResetFocus(ImGuiFullscreen::FocusResetType::Other); 2865 } 2866 2867 for (const bool show_all : {false, true}) 2868 { 2869 const char* title = 2870 show_all ? TRANSLATE("Achievements", "Show Best") : TRANSLATE("Achievements", "Show Nearby"); 2871 if (ImGuiFullscreen::NavTab(title, s_is_showing_all_leaderboard_entries == show_all, true, tab_width, 2872 tab_height_unscaled, heading_background)) 2873 { 2874 s_is_showing_all_leaderboard_entries = show_all; 2875 } 2876 } 2877 2878 const ImVec2 bg_pos = 2879 ImVec2(0.0f, ImGui::GetCurrentWindow()->DC.CursorPos.y + LayoutScale(tab_height_unscaled)); 2880 const ImVec2 bg_size = 2881 ImVec2(ImGui::GetWindowWidth(), 2882 spacing + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing); 2883 ImGui::GetWindowDrawList()->AddRectFilled(bg_pos, bg_pos + bg_size, ImGui::GetColorU32(heading_background)); 2884 2885 ImGui::SetCursorPos(ImVec2(0.0f, ImGui::GetCursorPosY() + LayoutScale(tab_height_unscaled) + spacing)); 2886 2887 pressed = 2888 ImGuiFullscreen::MenuButtonFrame("legend", false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, 2889 &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); 2890 UNREFERENCED_VARIABLE(pressed); 2891 2892 const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); 2893 float text_start_x = bb.Min.x + LayoutScale(15.0f) + padding; 2894 2895 ImGui::PushFont(g_large_font); 2896 2897 const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 2898 ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, TRANSLATE("Achievements", "Rank"), nullptr, nullptr, 2899 ImVec2(0.0f, 0.0f), &rank_bb); 2900 text_start_x += rank_column_width + column_spacing; 2901 2902 const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 2903 ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, TRANSLATE("Achievements", "Name"), nullptr, nullptr, 2904 ImVec2(0.0f, 0.0f), &user_bb); 2905 text_start_x += name_column_width + column_spacing; 2906 2907 static const char* value_headings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { 2908 TRANSLATE_NOOP("Achievements", "Time"), 2909 TRANSLATE_NOOP("Achievements", "Score"), 2910 TRANSLATE_NOOP("Achievements", "Value"), 2911 }; 2912 2913 const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 2914 ImGui::RenderTextClipped( 2915 score_bb.Min, score_bb.Max, 2916 Host::TranslateToCString( 2917 "Achievements", 2918 value_headings[std::min<u8>(s_open_leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)]), 2919 nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb); 2920 text_start_x += time_column_width + column_spacing; 2921 2922 const ImRect date_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 2923 ImGui::RenderTextClipped(date_bb.Min, date_bb.Max, TRANSLATE("Achievements", "Date Submitted"), nullptr, 2924 nullptr, ImVec2(0.0f, 0.0f), &date_bb); 2925 2926 ImGui::PopFont(); 2927 2928 const float line_thickness = LayoutScale(1.0f); 2929 const float line_padding = LayoutScale(5.0f); 2930 const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); 2931 const ImVec2 line_end(bb.Max.x, line_start.y); 2932 ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), 2933 line_thickness); 2934 } 2935 } 2936 } 2937 ImGuiFullscreen::EndFullscreenWindow(); 2938 FullscreenUI::SetStandardSelectionFooterText(true); 2939 2940 // See note in FullscreenUI::DrawSettingsWindow(). 2941 if (ImGuiFullscreen::IsFocusResetFromWindowChange()) 2942 ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); 2943 2944 if (!is_leaderboard_open) 2945 { 2946 if (ImGuiFullscreen::BeginFullscreenWindow( 2947 ImVec2(0.0f, heading_height), 2948 ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)), 2949 "leaderboards", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0)) 2950 { 2951 ImGuiFullscreen::ResetFocusHere(); 2952 ImGuiFullscreen::BeginMenuButtons(); 2953 2954 for (u32 bucket_index = 0; bucket_index < s_leaderboard_list->num_buckets; bucket_index++) 2955 { 2956 const rc_client_leaderboard_bucket_t& bucket = s_leaderboard_list->buckets[bucket_index]; 2957 for (u32 i = 0; i < bucket.num_leaderboards; i++) 2958 DrawLeaderboardListEntry(bucket.leaderboards[i]); 2959 } 2960 2961 ImGuiFullscreen::EndMenuButtons(); 2962 } 2963 ImGuiFullscreen::EndFullscreenWindow(); 2964 } 2965 else 2966 { 2967 if (ImGuiFullscreen::BeginFullscreenWindow( 2968 ImVec2(0.0f, heading_height), 2969 ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)), 2970 "leaderboard", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0)) 2971 { 2972 // Defer focus reset until loading finishes. 2973 if (!s_is_showing_all_leaderboard_entries || 2974 (ImGuiFullscreen::IsFocusResetFromWindowChange() && !s_leaderboard_entry_lists.empty())) 2975 { 2976 ImGuiFullscreen::ResetFocusHere(); 2977 } 2978 2979 ImGuiFullscreen::BeginMenuButtons(); 2980 2981 if (!s_is_showing_all_leaderboard_entries) 2982 { 2983 if (s_leaderboard_nearby_entries) 2984 { 2985 for (u32 i = 0; i < s_leaderboard_nearby_entries->num_entries; i++) 2986 { 2987 DrawLeaderboardEntry(s_leaderboard_nearby_entries->entries[i], 2988 static_cast<s32>(i) == s_leaderboard_nearby_entries->user_index, rank_column_width, 2989 name_column_width, time_column_width, column_spacing); 2990 } 2991 } 2992 else 2993 { 2994 ImGui::PushFont(g_large_font); 2995 2996 const ImVec2 pos_min(0.0f, heading_height); 2997 const ImVec2 pos_max(display_size.x, display_size.y); 2998 ImGui::RenderTextClipped(pos_min, pos_max, 2999 TRANSLATE("Achievements", "Downloading leaderboard data, please wait..."), nullptr, 3000 nullptr, ImVec2(0.5f, 0.5f)); 3001 3002 ImGui::PopFont(); 3003 } 3004 } 3005 else 3006 { 3007 for (const rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists) 3008 { 3009 for (u32 i = 0; i < list->num_entries; i++) 3010 { 3011 DrawLeaderboardEntry(list->entries[i], static_cast<s32>(i) == list->user_index, rank_column_width, 3012 name_column_width, time_column_width, column_spacing); 3013 } 3014 } 3015 3016 // Fetch next chunk if the loading indicator becomes visible (i.e. we scrolled enough). 3017 bool visible, hovered; 3018 ImGuiFullscreen::MenuButtonFrame(TRANSLATE("Achievements", "Loading..."), false, 3019 ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, 3020 &bb.Min, &bb.Max); 3021 if (visible) 3022 { 3023 const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); 3024 const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); 3025 3026 ImGui::PushFont(g_large_font); 3027 ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, TRANSLATE("Achievements", "Loading..."), nullptr, 3028 nullptr, ImVec2(0, 0), &title_bb); 3029 ImGui::PopFont(); 3030 3031 if (!s_leaderboard_fetch_handle) 3032 FetchNextLeaderboardEntries(); 3033 } 3034 } 3035 3036 ImGuiFullscreen::EndMenuButtons(); 3037 } 3038 ImGuiFullscreen::EndFullscreenWindow(); 3039 } 3040 3041 if (close_leaderboard_on_exit) 3042 CloseLeaderboard(); 3043 } 3044 3045 void Achievements::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self, 3046 float rank_column_width, float name_column_width, float time_column_width, 3047 float column_spacing) 3048 { 3049 using ImGuiFullscreen::g_large_font; 3050 using ImGuiFullscreen::LayoutScale; 3051 3052 static constexpr float alpha = 0.8f; 3053 3054 ImRect bb; 3055 bool visible, hovered; 3056 bool pressed = 3057 ImGuiFullscreen::MenuButtonFrame(entry.user, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, 3058 &hovered, &bb.Min, &bb.Max, 0, alpha); 3059 if (!visible) 3060 return; 3061 3062 const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); 3063 float text_start_x = bb.Min.x + LayoutScale(15.0f); 3064 SmallString text; 3065 3066 text.format("{}", entry.rank); 3067 3068 ImGui::PushFont(g_large_font); 3069 3070 if (is_self) 3071 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(255, 242, 0, 255)); 3072 3073 const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3074 ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f), 3075 &rank_bb); 3076 text_start_x += rank_column_width + column_spacing; 3077 3078 const float icon_size = bb.Max.y - bb.Min.y; 3079 const ImRect icon_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3080 GPUTexture* icon_tex = nullptr; 3081 if (auto it = std::find_if(s_leaderboard_user_icon_paths.begin(), s_leaderboard_user_icon_paths.end(), 3082 [&entry](const auto& it) { return it.first == &entry; }); 3083 it != s_leaderboard_user_icon_paths.end()) 3084 { 3085 if (!it->second.empty()) 3086 icon_tex = ImGuiFullscreen::GetCachedTextureAsync(it->second); 3087 } 3088 else 3089 { 3090 std::string path = Achievements::GetLeaderboardUserBadgePath(&entry); 3091 if (!path.empty()) 3092 { 3093 icon_tex = ImGuiFullscreen::GetCachedTextureAsync(path); 3094 s_leaderboard_user_icon_paths.emplace_back(&entry, std::move(path)); 3095 } 3096 } 3097 if (icon_tex) 3098 { 3099 ImGui::GetWindowDrawList()->AddImage(reinterpret_cast<ImTextureID>(icon_tex), icon_bb.Min, 3100 icon_bb.Min + ImVec2(icon_size, icon_size)); 3101 } 3102 3103 const ImRect user_bb(ImVec2(text_start_x + column_spacing + icon_size, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3104 ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, entry.user, nullptr, nullptr, ImVec2(0.0f, 0.0f), &user_bb); 3105 text_start_x += name_column_width + column_spacing; 3106 3107 const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3108 ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, entry.display, nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb); 3109 text_start_x += time_column_width + column_spacing; 3110 3111 const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3112 SmallString submit_time; 3113 FullscreenUI::TimeToPrintableString(&submit_time, entry.submitted); 3114 ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, submit_time.c_str(), submit_time.end_ptr(), nullptr, 3115 ImVec2(0.0f, 0.0f), &time_bb); 3116 3117 if (is_self) 3118 ImGui::PopStyleColor(); 3119 3120 ImGui::PopFont(); 3121 3122 if (pressed) 3123 { 3124 // Anything? 3125 } 3126 } 3127 void Achievements::DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard) 3128 { 3129 using ImGuiFullscreen::g_large_font; 3130 using ImGuiFullscreen::g_medium_font; 3131 using ImGuiFullscreen::LayoutScale; 3132 3133 static constexpr float alpha = 0.8f; 3134 3135 TinyString id_str; 3136 id_str.format("{}", lboard->id); 3137 3138 ImRect bb; 3139 bool visible, hovered; 3140 bool pressed = ImGuiFullscreen::MenuButtonFrame(id_str, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible, 3141 &hovered, &bb.Min, &bb.Max, 0, alpha); 3142 if (!visible) 3143 return; 3144 3145 const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); 3146 const float text_start_x = bb.Min.x + LayoutScale(15.0f); 3147 const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); 3148 const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); 3149 3150 ImGui::PushFont(g_large_font); 3151 ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, lboard->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); 3152 ImGui::PopFont(); 3153 3154 if (lboard->description && lboard->description[0] != '\0') 3155 { 3156 ImGui::PushFont(g_medium_font); 3157 ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, lboard->description, nullptr, nullptr, ImVec2(0.0f, 0.0f), 3158 &summary_bb); 3159 ImGui::PopFont(); 3160 } 3161 3162 if (pressed) 3163 OpenLeaderboard(lboard); 3164 } 3165 3166 #endif // __ANDROID__ 3167 3168 void Achievements::OpenLeaderboard(const rc_client_leaderboard_t* lboard) 3169 { 3170 DEV_LOG("Opening leaderboard '{}' ({})", lboard->title, lboard->id); 3171 3172 CloseLeaderboard(); 3173 3174 s_open_leaderboard = lboard; 3175 s_is_showing_all_leaderboard_entries = false; 3176 s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries_around_user( 3177 s_client, lboard->id, LEADERBOARD_NEARBY_ENTRIES_TO_FETCH, LeaderboardFetchNearbyCallback, nullptr); 3178 ImGuiFullscreen::QueueResetFocus(ImGuiFullscreen::FocusResetType::Other); 3179 } 3180 3181 bool Achievements::OpenLeaderboardById(u32 leaderboard_id) 3182 { 3183 const rc_client_leaderboard_t* lb = rc_client_get_leaderboard_info(s_client, leaderboard_id); 3184 if (!lb) 3185 return false; 3186 3187 OpenLeaderboard(lb); 3188 return true; 3189 } 3190 3191 u32 Achievements::GetOpenLeaderboardId() 3192 { 3193 return s_open_leaderboard ? s_open_leaderboard->id : 0; 3194 } 3195 3196 bool Achievements::IsShowingAllLeaderboardEntries() 3197 { 3198 return s_is_showing_all_leaderboard_entries; 3199 } 3200 3201 const std::vector<rc_client_leaderboard_entry_list_t*>& Achievements::GetLeaderboardEntryLists() 3202 { 3203 return s_leaderboard_entry_lists; 3204 } 3205 3206 const rc_client_leaderboard_entry_list_t* Achievements::GetLeaderboardNearbyEntries() 3207 { 3208 return s_leaderboard_nearby_entries; 3209 } 3210 3211 void Achievements::LeaderboardFetchNearbyCallback(int result, const char* error_message, 3212 rc_client_leaderboard_entry_list_t* list, rc_client_t* client, 3213 void* callback_userdata) 3214 { 3215 const auto lock = GetLock(); 3216 3217 s_leaderboard_fetch_handle = nullptr; 3218 3219 if (result != RC_OK) 3220 { 3221 ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message); 3222 CloseLeaderboard(); 3223 return; 3224 } 3225 3226 if (s_leaderboard_nearby_entries) 3227 rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries); 3228 s_leaderboard_nearby_entries = list; 3229 } 3230 3231 void Achievements::LeaderboardFetchAllCallback(int result, const char* error_message, 3232 rc_client_leaderboard_entry_list_t* list, rc_client_t* client, 3233 void* callback_userdata) 3234 { 3235 const auto lock = GetLock(); 3236 3237 s_leaderboard_fetch_handle = nullptr; 3238 3239 if (result != RC_OK) 3240 { 3241 ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message); 3242 CloseLeaderboard(); 3243 return; 3244 } 3245 3246 s_leaderboard_entry_lists.push_back(list); 3247 } 3248 3249 void Achievements::FetchNextLeaderboardEntries() 3250 { 3251 u32 start = 1; 3252 for (rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists) 3253 start += list->num_entries; 3254 3255 DEV_LOG("Fetching entries {} to {}", start, start + LEADERBOARD_ALL_FETCH_SIZE); 3256 3257 if (s_leaderboard_fetch_handle) 3258 rc_client_abort_async(s_client, s_leaderboard_fetch_handle); 3259 s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries( 3260 s_client, s_open_leaderboard->id, start, LEADERBOARD_ALL_FETCH_SIZE, LeaderboardFetchAllCallback, nullptr); 3261 } 3262 3263 void Achievements::CloseLeaderboard() 3264 { 3265 s_leaderboard_user_icon_paths.clear(); 3266 3267 for (auto iter = s_leaderboard_entry_lists.rbegin(); iter != s_leaderboard_entry_lists.rend(); ++iter) 3268 rc_client_destroy_leaderboard_entry_list(*iter); 3269 s_leaderboard_entry_lists.clear(); 3270 3271 if (s_leaderboard_nearby_entries) 3272 { 3273 rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries); 3274 s_leaderboard_nearby_entries = nullptr; 3275 } 3276 3277 if (s_leaderboard_fetch_handle) 3278 { 3279 rc_client_abort_async(s_client, s_leaderboard_fetch_handle); 3280 s_leaderboard_fetch_handle = nullptr; 3281 } 3282 3283 s_open_leaderboard = nullptr; 3284 ImGuiFullscreen::QueueResetFocus(ImGuiFullscreen::FocusResetType::Other); 3285 } 3286 3287 #ifdef ENABLE_RAINTEGRATION 3288 3289 #include "RA_Consoles.h" 3290 3291 bool Achievements::IsUsingRAIntegration() 3292 { 3293 return s_using_raintegration; 3294 } 3295 3296 namespace Achievements::RAIntegration { 3297 static void InitializeRAIntegration(void* main_window_handle); 3298 3299 static int RACallbackIsActive(); 3300 static void RACallbackCauseUnpause(); 3301 static void RACallbackCausePause(); 3302 static void RACallbackRebuildMenu(); 3303 static void RACallbackEstimateTitle(char* buf); 3304 static void RACallbackResetEmulator(); 3305 static void RACallbackLoadROM(const char* unused); 3306 static unsigned char RACallbackReadRAM(unsigned int address); 3307 static unsigned int RACallbackReadRAMBlock(unsigned int nAddress, unsigned char* pBuffer, unsigned int nBytes); 3308 static void RACallbackWriteRAM(unsigned int address, unsigned char value); 3309 static unsigned char RACallbackReadScratchpad(unsigned int address); 3310 static unsigned int RACallbackReadScratchpadBlock(unsigned int nAddress, unsigned char* pBuffer, unsigned int nBytes); 3311 static void RACallbackWriteScratchpad(unsigned int address, unsigned char value); 3312 3313 static bool s_raintegration_initialized = false; 3314 } // namespace Achievements::RAIntegration 3315 3316 void Achievements::SwitchToRAIntegration() 3317 { 3318 s_using_raintegration = true; 3319 } 3320 3321 void Achievements::RAIntegration::InitializeRAIntegration(void* main_window_handle) 3322 { 3323 RA_InitClient((HWND)main_window_handle, "DuckStation", g_scm_tag_str); 3324 RA_SetUserAgentDetail(Host::GetHTTPUserAgent().c_str()); 3325 3326 RA_InstallSharedFunctions(RACallbackIsActive, RACallbackCauseUnpause, RACallbackCausePause, RACallbackRebuildMenu, 3327 RACallbackEstimateTitle, RACallbackResetEmulator, RACallbackLoadROM); 3328 RA_SetConsoleID(PlayStation); 3329 3330 // Apparently this has to be done early, or the memory inspector doesn't work. 3331 // That's a bit unfortunate, because the RAM size can vary between games, and depending on the option. 3332 RA_InstallMemoryBank(0, RACallbackReadRAM, RACallbackWriteRAM, Bus::RAM_2MB_SIZE); 3333 RA_InstallMemoryBankBlockReader(0, RACallbackReadRAMBlock); 3334 RA_InstallMemoryBank(1, RACallbackReadScratchpad, RACallbackWriteScratchpad, CPU::SCRATCHPAD_SIZE); 3335 RA_InstallMemoryBankBlockReader(1, RACallbackReadScratchpadBlock); 3336 3337 // Fire off a login anyway. Saves going into the menu and doing it. 3338 RA_AttemptLogin(0); 3339 3340 s_raintegration_initialized = true; 3341 3342 // this is pretty lame, but we may as well persist until we exit anyway 3343 std::atexit(RA_Shutdown); 3344 } 3345 3346 void Achievements::RAIntegration::MainWindowChanged(void* new_handle) 3347 { 3348 if (s_raintegration_initialized) 3349 { 3350 RA_UpdateHWnd((HWND)new_handle); 3351 return; 3352 } 3353 3354 InitializeRAIntegration(new_handle); 3355 } 3356 3357 void Achievements::RAIntegration::GameChanged() 3358 { 3359 s_game_id = s_game_hash.empty() ? 0 : RA_IdentifyHash(s_game_hash.c_str()); 3360 RA_ActivateGame(s_game_id); 3361 } 3362 3363 std::vector<std::tuple<int, std::string, bool>> Achievements::RAIntegration::GetMenuItems() 3364 { 3365 std::array<RA_MenuItem, 64> items; 3366 const int num_items = RA_GetPopupMenuItems(items.data()); 3367 3368 std::vector<std::tuple<int, std::string, bool>> ret; 3369 ret.reserve(static_cast<u32>(num_items)); 3370 3371 for (int i = 0; i < num_items; i++) 3372 { 3373 const RA_MenuItem& it = items[i]; 3374 if (!it.sLabel) 3375 ret.emplace_back(0, std::string(), false); 3376 else 3377 ret.emplace_back(static_cast<int>(it.nID), StringUtil::WideStringToUTF8String(it.sLabel), it.bChecked); 3378 } 3379 3380 return ret; 3381 } 3382 3383 void Achievements::RAIntegration::ActivateMenuItem(int item) 3384 { 3385 RA_InvokeDialog(item); 3386 } 3387 3388 int Achievements::RAIntegration::RACallbackIsActive() 3389 { 3390 return static_cast<int>(HasActiveGame()); 3391 } 3392 3393 void Achievements::RAIntegration::RACallbackCauseUnpause() 3394 { 3395 Host::RunOnCPUThread([]() { System::PauseSystem(false); }); 3396 } 3397 3398 void Achievements::RAIntegration::RACallbackCausePause() 3399 { 3400 Host::RunOnCPUThread([]() { System::PauseSystem(true); }); 3401 } 3402 3403 void Achievements::RAIntegration::RACallbackRebuildMenu() 3404 { 3405 // unused, we build the menu on demand 3406 } 3407 3408 void Achievements::RAIntegration::RACallbackEstimateTitle(char* buf) 3409 { 3410 StringUtil::Strlcpy(buf, System::GetGameTitle(), 256); 3411 } 3412 3413 void Achievements::RAIntegration::RACallbackResetEmulator() 3414 { 3415 if (System::IsValid()) 3416 System::ResetSystem(); 3417 } 3418 3419 void Achievements::RAIntegration::RACallbackLoadROM(const char* unused) 3420 { 3421 // unused 3422 UNREFERENCED_PARAMETER(unused); 3423 } 3424 3425 unsigned char Achievements::RAIntegration::RACallbackReadRAM(unsigned int address) 3426 { 3427 if (!System::IsValid()) 3428 return 0; 3429 3430 u8 value = 0; 3431 CPU::SafeReadMemoryByte(address, &value); 3432 return value; 3433 } 3434 3435 void Achievements::RAIntegration::RACallbackWriteRAM(unsigned int address, unsigned char value) 3436 { 3437 CPU::SafeWriteMemoryByte(address, value); 3438 } 3439 3440 unsigned int Achievements::RAIntegration::RACallbackReadRAMBlock(unsigned int nAddress, unsigned char* pBuffer, 3441 unsigned int nBytes) 3442 { 3443 if (nAddress >= Bus::g_ram_size) 3444 return 0; 3445 3446 const u32 copy_size = std::min<u32>(Bus::g_ram_size - nAddress, nBytes); 3447 std::memcpy(pBuffer, Bus::g_unprotected_ram + nAddress, copy_size); 3448 return copy_size; 3449 } 3450 3451 unsigned char Achievements::RAIntegration::RACallbackReadScratchpad(unsigned int address) 3452 { 3453 if (!System::IsValid() || address >= CPU::SCRATCHPAD_SIZE) 3454 return 0; 3455 3456 return CPU::g_state.scratchpad[address]; 3457 } 3458 3459 void Achievements::RAIntegration::RACallbackWriteScratchpad(unsigned int address, unsigned char value) 3460 { 3461 if (address >= CPU::SCRATCHPAD_SIZE) 3462 return; 3463 3464 CPU::g_state.scratchpad[address] = value; 3465 } 3466 3467 unsigned int Achievements::RAIntegration::RACallbackReadScratchpadBlock(unsigned int nAddress, unsigned char* pBuffer, 3468 unsigned int nBytes) 3469 { 3470 if (nAddress >= CPU::SCRATCHPAD_SIZE) 3471 return 0; 3472 3473 const u32 copy_size = std::min<u32>(CPU::SCRATCHPAD_SIZE - nAddress, nBytes); 3474 std::memcpy(pBuffer, &CPU::g_state.scratchpad[nAddress], copy_size); 3475 return copy_size; 3476 } 3477 3478 #else 3479 3480 bool Achievements::IsUsingRAIntegration() 3481 { 3482 return false; 3483 } 3484 3485 #endif