diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ce97fc15..f3d78fbf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,6 @@ option(ENABLE_CUBEB "Build with Cubeb audio output" ON) option(ENABLE_OPENGL "Build with OpenGL renderer" ON) option(ENABLE_VULKAN "Build with Vulkan renderer" ON) option(ENABLE_DISCORD_PRESENCE "Build with Discord Rich Presence support" ON) -option(ENABLE_CHEEVOS "Build with RetroAchievements support" ON) #option(USE_SDL2 "Link with SDL2 for controller support" ON) set(USE_SDL2 ON) diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 5ddd5f032..86969f8f5 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(googletest EXCLUDE_FROM_ALL) add_subdirectory(cpuinfo EXCLUDE_FROM_ALL) add_subdirectory(fast_float EXCLUDE_FROM_ALL) add_subdirectory(reshadefx EXCLUDE_FROM_ALL) +add_subdirectory(rcheevos EXCLUDE_FROM_ALL) if(ENABLE_CUBEB) add_subdirectory(cubeb EXCLUDE_FROM_ALL) @@ -29,10 +30,6 @@ if(ENABLE_DISCORD_PRESENCE) add_subdirectory(discord-rpc EXCLUDE_FROM_ALL) endif() -if(ENABLE_CHEEVOS) - add_subdirectory(rcheevos EXCLUDE_FROM_ALL) -endif() - if(${CPU_ARCH} STREQUAL "aarch32" OR ${CPU_ARCH} STREQUAL "aarch64") add_subdirectory(vixl EXCLUDE_FROM_ALL) endif() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ad22b468d..e2ea01b38 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(core + achievements.cpp achievements.h analog_controller.cpp analog_controller.h @@ -123,7 +124,7 @@ target_precompile_headers(core PRIVATE "pch.h") target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_link_libraries(core PUBLIC Threads::Threads common util zlib) -target_link_libraries(core PRIVATE stb xxhash imgui rapidjson) +target_link_libraries(core PRIVATE stb xxhash imgui rapidjson rcheevos) if(${CPU_ARCH} STREQUAL "x64") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../dep/xbyak/xbyak") @@ -154,14 +155,6 @@ else() message("Not building recompiler") endif() -if(ENABLE_CHEEVOS) - target_sources(core PRIVATE - achievements.cpp - ) - target_compile_definitions(core PUBLIC -DWITH_CHEEVOS=1) - target_link_libraries(core PRIVATE rcheevos rapidjson) -endif() - if(ENABLE_DISCORD_PRESENCE) target_compile_definitions(core PUBLIC -DWITH_DISCORD_PRESENCE=1) target_link_libraries(core PRIVATE discord-rpc) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 9d74b5299..2da198222 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -1,6 +1,11 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) +// TODO: Don't poll when booting the game, e.g. Crash Warped freaks out. +// TODO: rc_client_begin_change_media + +#define IMGUI_DEFINE_MATH_OPERATORS + #include "achievements.h" #include "bios.h" #include "bus.h" @@ -12,12 +17,15 @@ #include "scmversion/scmversion.h" #include "common/assert.h" +#include "common/error.h" #include "common/file_system.h" #include "common/http_downloader.h" #include "common/log.h" #include "common/md5_digest.h" #include "common/path.h" #include "common/platform.h" +#include "common/scoped_guard.h" +#include "common/string.h" #include "common/string_util.h" #include "util/cd_image.h" @@ -26,13 +34,12 @@ #include "util/platform_misc.h" #include "util/state_wrapper.h" -#include "rapidjson/document.h" -#include "rc_api_info.h" -#include "rc_api_request.h" -#include "rc_api_runtime.h" -#include "rc_api_user.h" -#include "rc_url.h" -#include "rcheevos.h" +#include "IconsFontAwesome5.h" +#include "fmt/format.h" +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_stdlib.h" +#include "rc_client.h" #include #include @@ -53,427 +60,392 @@ Log_SetChannel(Achievements); #include "RA_Interface.h" #endif namespace Achievements { -enum : s32 -{ - HTTP_OK = Common::HTTPDownloader::HTTP_OK, - - // Number of seconds between rich presence pings. RAIntegration uses 2 minutes. - RICH_PRESENCE_PING_FREQUENCY = 2 * 60, - NO_RICH_PRESENCE_PING_FREQUENCY = RICH_PRESENCE_PING_FREQUENCY * 2, -}; static constexpr const char* INFO_SOUND_NAME = "sounds/achievements/message.wav"; static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav"; static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav"; -static void FormattedError(const char* format, ...); -static void LogFailedResponseJSON(const Common::HTTPDownloader::Request::Data& data); +static constexpr u32 LEADERBOARD_NEARBY_ENTRIES_TO_FETCH = 10; +static constexpr u32 LEADERBOARD_ALL_FETCH_SIZE = 20; + +static constexpr float LOGIN_NOTIFICATION_TIME = 5.0f; +static constexpr float ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME = 5.0f; +static constexpr float GAME_COMPLETE_NOTIFICATION_TIME = 20.0f; +static constexpr float LEADERBOARD_STARTED_NOTIFICATION_TIME = 3.0f; +static constexpr float LEADERBOARD_FAILED_NOTIFICATION_TIME = 3.0f; + +static constexpr float INDICATOR_FADE_IN_TIME = 0.1f; +static constexpr float INDICATOR_FADE_OUT_TIME = 0.5f; + +namespace { +struct LoginWithPasswordParameters +{ + const char* username; + Error* error; + rc_client_async_handle_t* request; + bool result; +}; +struct LeaderboardTrackerIndicator +{ + u32 tracker_id; + ImVec2 size; + std::string text; + Common::Timer show_hide_time; + bool active; +}; + +struct AchievementChallengeIndicator +{ + const rc_client_achievement_t* achievement; + std::string badge_path; + Common::Timer show_hide_time; + bool active; +}; + +struct AchievementProgressIndicator +{ + const rc_client_achievement_t* achievement; + std::string badge_path; + Common::Timer show_hide_time; + bool active; +}; +} // namespace + +static void ReportError(const std::string_view& sv); +template +static void ReportFmtError(fmt::format_string fmt, T&&... args); +template +static void ReportRCError(int err, fmt::format_string fmt, T&&... args); static void EnsureCacheDirectoriesExist(); -static void CheevosEventHandler(const rc_runtime_event_t* runtime_event); -static unsigned PeekMemory(unsigned address, unsigned num_bytes, void* ud); -static bool IsMastered(); -static void ActivateLockedAchievements(); -static bool ActivateAchievement(Achievement* achievement); -static void DeactivateAchievement(Achievement* achievement); -static void UnlockAchievement(u32 achievement_id, bool add_notification = true); -static void AchievementPrimed(u32 achievement_id); -static void AchievementUnprimed(u32 achievement_id); -static void SubmitLeaderboard(u32 leaderboard_id, int value); -static void SendPing(); -static void SendPlaying(); -static void UpdateRichPresence(); -static Achievement* GetMutableAchievementByID(u32 id); -static void ClearGameInfo(bool clear_achievements = true, bool clear_leaderboards = true); +static void ClearGameInfo(); static void ClearGameHash(); static std::string GetUserAgent(); -static void LoginCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void LoginASyncCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void SendLogin(const char* username, const char* password, Common::HTTPDownloader* http_downloader, - Common::HTTPDownloader::Request::Callback callback); -static void DownloadImage(std::string url, std::string cache_filename); -static void DisplayAchievementSummary(); -static void DisplayMasteredNotification(); -static void GetUserUnlocksCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data); -static void GetUserUnlocks(); -static void GetPatchesCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void GetLbInfoCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void GetPatches(u32 game_id); static std::string GetGameHash(CDImage* image); static void SetChallengeMode(bool enabled); -static void SendGetGameId(); -static void GetGameIdCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void SendPlayingCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void UpdateRichPresence(); -static void SendPingCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); -static void UnlockAchievementCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data); -static void SubmitLeaderboardCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data, u32 lboard_id); - -static bool s_active = false; -static bool s_logged_in = false; -static bool s_challenge_mode = false; -static u32 s_game_id = 0; +static bool IsLoggedIn(); +static void ShowLoginSuccess(const rc_client_t* client); +static void ShowLoginNotification(); +static void CancelGameLoad(); +static void IdentifyGame(const std::string& path, CDImage* image); +static void BeginLoadGame(); +static void UpdateGameSummary(); +static void DownloadImage(std::string url, std::string cache_filename); + +static bool CreateClient(rc_client_t** client, std::unique_ptr* http); +static void DestroyClient(rc_client_t** client, std::unique_ptr* http); +static void ClientMessageCallback(const char* message, const rc_client_t* client); +static uint32_t ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); +static void ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, + rc_client_t* client); + +static void ClientEventHandler(const rc_client_event_t* event, rc_client_t* client); +static void HandleResetEvent(const rc_client_event_t* event); +static void HandleUnlockEvent(const rc_client_event_t* event); +static void HandleGameCompleteEvent(const rc_client_event_t* event); +static void HandleLeaderboardStartedEvent(const rc_client_event_t* event); +static void HandleLeaderboardFailedEvent(const rc_client_event_t* event); +static void HandleLeaderboardSubmittedEvent(const rc_client_event_t* event); +static void HandleLeaderboardScoreboardEvent(const rc_client_event_t* event); +static void HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event); +static void HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event); +static void HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event); +static void HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event); +static void HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event); +static void HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event); +static void HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event); +static void HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event); +static void HandleServerErrorEvent(const rc_client_event_t* event); +static void HandleServerDisconnectedEvent(const rc_client_event_t* event); +static void HandleServerReconnectedEvent(const rc_client_event_t* event); + +static void ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void* userdata); +static void ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, void* userdata); +static void ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata); + +static void DisplayAchievementSummary(); +static void UpdateRichPresence(std::unique_lock& lock); + +static std::string GetAchievementBadgePath(const rc_client_achievement_t* achievement, int state); +static std::string GetUserBadgePath(const std::string_view& username); +static std::string GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry); + +static void DrawAchievement(const rc_client_achievement_t* cheevo); +static void DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard); +static void DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self, float rank_column_width, + float name_column_width, float time_column_width, float column_spacing); +static void OpenLeaderboard(const rc_client_leaderboard_t* lboard); +static void LeaderboardFetchNearbyCallback(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, + void* callback_userdata); +static void LeaderboardFetchAllCallback(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, + rc_client_t* client, void* callback_userdata); +static void FetchNextLeaderboardEntries(); +static void CloseLeaderboard(); + +static bool s_hardcore_mode = false; #ifdef WITH_RAINTEGRATION static bool s_using_raintegration = false; #endif static std::recursive_mutex s_achievements_mutex; -static rc_runtime_t s_rcheevos_runtime; -static std::string s_game_icon_cache_directory; -static std::string s_achievement_icon_cache_directory; +static rc_client_t* s_client; +static std::string s_image_directory; static std::unique_ptr s_http_downloader; -static std::string s_username; -static std::string s_api_token; - static std::string s_game_path; static std::string s_game_hash; static std::string s_game_title; static std::string s_game_icon; -static std::vector s_achievements; -static std::vector s_leaderboards; -static std::atomic s_primed_achievement_count{0}; +static rc_client_user_game_summary_t s_game_summary; +static u32 s_game_id = 0; +static bool s_has_achievements = false; +static bool s_has_leaderboards = false; static bool s_has_rich_presence = false; static std::string s_rich_presence_string; -static Common::Timer s_last_ping_time; - -static u32 s_last_queried_lboard = 0; -static std::optional> s_lboard_entries; - -template -static const char* RAPIStructName(); - -#define RAPI_STRUCT_NAME(x) \ - template<> \ - const char* RAPIStructName() \ - { \ - return #x; \ - } - -RAPI_STRUCT_NAME(rc_api_login_request_t); -RAPI_STRUCT_NAME(rc_api_fetch_image_request_t); -RAPI_STRUCT_NAME(rc_api_resolve_hash_request_t); -RAPI_STRUCT_NAME(rc_api_fetch_game_data_request_t); -RAPI_STRUCT_NAME(rc_api_fetch_user_unlocks_request_t); -RAPI_STRUCT_NAME(rc_api_start_session_request_t); -RAPI_STRUCT_NAME(rc_api_ping_request_t); -RAPI_STRUCT_NAME(rc_api_award_achievement_request_t); -RAPI_STRUCT_NAME(rc_api_submit_lboard_entry_request_t); -RAPI_STRUCT_NAME(rc_api_fetch_leaderboard_info_request_t); - -RAPI_STRUCT_NAME(rc_api_login_response_t); -RAPI_STRUCT_NAME(rc_api_resolve_hash_response_t); -RAPI_STRUCT_NAME(rc_api_fetch_game_data_response_t); -RAPI_STRUCT_NAME(rc_api_ping_response_t); -RAPI_STRUCT_NAME(rc_api_award_achievement_response_t); -RAPI_STRUCT_NAME(rc_api_submit_lboard_entry_response_t); -RAPI_STRUCT_NAME(rc_api_start_session_response_t); -RAPI_STRUCT_NAME(rc_api_fetch_user_unlocks_response_t); -RAPI_STRUCT_NAME(rc_api_fetch_leaderboard_info_response_t); - -// Unused for now. -// RAPI_STRUCT_NAME(rc_api_fetch_achievement_info_response_t); -// RAPI_STRUCT_NAME(rc_api_fetch_games_list_response_t); - -#undef RAPI_STRUCT_NAME - -template -struct RAPIRequest : public T -{ -private: - rc_api_request_t api_request; - -public: - RAPIRequest() { std::memset(this, 0, sizeof(*this)); } - - ~RAPIRequest() { rc_api_destroy_request(&api_request); } - - void Send(Common::HTTPDownloader::Request::Callback callback) { Send(s_http_downloader.get(), std::move(callback)); } - - void Send(Common::HTTPDownloader* http_downloader, Common::HTTPDownloader::Request::Callback callback) - { - const int error = InitFunc(&api_request, this); - if (error != RC_OK) - { - FormattedError("%s failed: error %d (%s)", RAPIStructName(), error, rc_error_str(error)); - callback(-1, std::string(), Common::HTTPDownloader::Request::Data()); - return; - } - - if (api_request.post_data) - { - // needs to be a post - http_downloader->CreatePostRequest(api_request.url, api_request.post_data, std::move(callback)); - } - else - { - // get is fine - http_downloader->CreateRequest(api_request.url, std::move(callback)); - } - } - - bool DownloadImage(std::string cache_filename) - { - const int error = InitFunc(&api_request, this); - if (error != RC_OK) - { - FormattedError("%s failed: error %d (%s)", RAPIStructName(), error, rc_error_str(error)); - return false; - } - - DebugAssertMsg(!api_request.post_data, "Download request does not have POST data"); - Achievements::DownloadImage(api_request.url, std::move(cache_filename)); - return true; - } - - std::string GetURL() - { - const int error = InitFunc(&api_request, this); - if (error != RC_OK) - { - FormattedError("%s failed: error %d (%s)", RAPIStructName(), error, rc_error_str(error)); - return std::string(); - } - - return api_request.url; - } -}; - -template -struct RAPIResponse : public T -{ -private: - bool initialized = false; - -public: - RAPIResponse(s32 status_code, Common::HTTPDownloader::Request::Data& data) - { - if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty()) - { - FormattedError("%s failed: empty response and/or status code %d", RAPIStructName(), status_code); - LogFailedResponseJSON(data); - return; - } - - // ensure null termination, rapi needs it - data.push_back(0); - - const int error = ParseFunc(this, reinterpret_cast(data.data())); - initialized = (error == RC_OK); - - const rc_api_response_t& response = static_cast(this)->response; - if (error != RC_OK) - { - FormattedError("%s failed: parse function returned %d (%s)", RAPIStructName(), error, rc_error_str(error)); - LogFailedResponseJSON(data); - } - else if (!response.succeeded) - { - FormattedError("%s failed: %s", RAPIStructName(), - response.error_message ? response.error_message : ""); - LogFailedResponseJSON(data); - } - } - - ~RAPIResponse() - { - if (initialized) - DestroyFunc(this); - } - - operator bool() const { return initialized && static_cast(this)->response.succeeded; } -}; - +static Common::Timer s_rich_presence_poll_time; + +static rc_client_async_handle_t* s_login_request; +static rc_client_async_handle_t* s_load_game_request; + +static rc_client_achievement_list_t* s_achievement_list; +static rc_client_leaderboard_list_t* s_leaderboard_list; +static std::vector> s_achievement_badge_paths; +static const rc_client_leaderboard_t* s_open_leaderboard = nullptr; +static rc_client_async_handle_t* s_leaderboard_fetch_handle = nullptr; +static std::vector s_leaderboard_entry_lists; +static rc_client_leaderboard_entry_list_t* s_leaderboard_nearby_entries; +static std::vector> s_leaderboard_user_icon_paths; +static bool s_is_showing_all_leaderboard_entries = false; + +static std::vector s_active_leaderboard_trackers; +static std::vector s_active_challenge_indicators; +static std::optional s_active_progress_indicator; } // namespace Achievements -#ifdef WITH_RAINTEGRATION -bool Achievements::IsUsingRAIntegration() +std::unique_lock Achievements::GetLock() { - return s_using_raintegration; + return std::unique_lock(s_achievements_mutex); } -#endif -void Achievements::FormattedError(const char* format, ...) +std::string Achievements::GetUserAgent() { - std::va_list ap; - va_start(ap, format); - std::string error(fmt::format("Achievements error: {}", StringUtil::StdStringFromFormatV(format, ap))); - va_end(ap); + return fmt::format("DuckStation for {} ({}) {}", SYSTEM_STR, CPU_ARCH_STR, g_scm_tag_str); +} +void Achievements::ReportError(const std::string_view& sv) +{ + std::string error = fmt::format("Achievements error: {}", sv); Log_ErrorPrint(error.c_str()); - Host::AddOSDMessage(std::move(error), 10.0f); + Host::AddOSDMessage(std::move(error), Host::OSD_CRITICAL_ERROR_DURATION); } -void Achievements::LogFailedResponseJSON(const Common::HTTPDownloader::Request::Data& data) +template +void Achievements::ReportFmtError(fmt::format_string fmt, T&&... args) { - const std::string str_data(reinterpret_cast(data.data()), data.size()); - Log_ErrorPrintf("API call failed. Response JSON was:\n%s", str_data.c_str()); + TinyString str; + fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...)); + ReportError(str); } -const Achievements::Achievement* Achievements::GetAchievementByID(u32 id) +template +void Achievements::ReportRCError(int err, fmt::format_string fmt, T&&... args) { - for (const Achievement& ach : s_achievements) - { - if (ach.id == id) - return &ach; - } - - return nullptr; + TinyString str; + fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...)); + str.AppendFmtString("{} ({})", rc_error_str(err), err); + ReportError(str); } -Achievements::Achievement* Achievements::GetMutableAchievementByID(u32 id) +std::string Achievements::GetGameHash(CDImage* image) { - for (Achievement& ach : s_achievements) + std::string executable_name; + std::vector executable_data; + if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) + return {}; + + BIOS::PSEXEHeader header; + if (executable_data.size() >= sizeof(header)) + std::memcpy(&header, executable_data.data(), sizeof(header)); + if (!BIOS::IsValidPSExeHeader(header, static_cast(executable_data.size()))) { - if (ach.id == id) - return &ach; + Log_ErrorPrintf("PS-EXE header is invalid in '%s' (%zu bytes)", executable_name.c_str(), executable_data.size()); + return {}; } - return nullptr; + // See rcheevos hash.c - rc_hash_psx(). + const u32 MAX_HASH_SIZE = 64 * 1024 * 1024; + const u32 hash_size = std::min(sizeof(header) + header.file_size, MAX_HASH_SIZE); + Assert(hash_size <= executable_data.size()); + + MD5Digest digest; + digest.Update(executable_name.c_str(), static_cast(executable_name.size())); + if (hash_size > 0) + digest.Update(executable_data.data(), hash_size); + + u8 hash[16]; + digest.Final(hash); + + std::string hash_str(StringUtil::StdStringFromFormat( + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", hash[0], hash[1], hash[2], hash[3], hash[4], + hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15])); + + Log_InfoPrintf("Hash for '%s' (%zu bytes, %u bytes hashed): %s", executable_name.c_str(), executable_data.size(), + hash_size, hash_str.c_str()); + return hash_str; } -void Achievements::ClearGameInfo(bool clear_achievements, bool clear_leaderboards) +void Achievements::DownloadImage(std::string url, std::string cache_filename) { - const bool had_game = (s_game_id != 0); + auto callback = [cache_filename](s32 status_code, std::string content_type, + Common::HTTPDownloader::Request::Data data) { + if (status_code != Common::HTTPDownloader::HTTP_OK) + return; - if (clear_achievements) - { - while (!s_achievements.empty()) - { - Achievement& ach = s_achievements.back(); - DeactivateAchievement(&ach); - s_achievements.pop_back(); - } - s_primed_achievement_count.store(0, std::memory_order_release); - } - if (clear_leaderboards) - { - while (!s_leaderboards.empty()) + if (!FileSystem::WriteBinaryFile(cache_filename.c_str(), data.data(), data.size())) { - Leaderboard& lb = s_leaderboards.back(); - rc_runtime_deactivate_lboard(&s_rcheevos_runtime, lb.id); - s_leaderboards.pop_back(); + Log_ErrorPrintf("Failed to write badge image to '%s'", cache_filename.c_str()); + return; } - s_last_queried_lboard = 0; - s_lboard_entries.reset(); - } - - if (s_achievements.empty() && s_leaderboards.empty()) - { - // Ready to tear down cheevos completely - s_game_title = {}; - s_game_icon = {}; - s_rich_presence_string = {}; - s_has_rich_presence = false; - s_game_id = 0; - } - - if (had_game) - Host::OnAchievementsRefreshed(); -} + ImGuiFullscreen::InvalidateCachedTexture(cache_filename); + }; -void Achievements::ClearGameHash() -{ - s_game_path = {}; - std::string().swap(s_game_hash); + s_http_downloader->CreateRequest(std::move(url), std::move(callback)); } -std::string Achievements::GetUserAgent() +bool Achievements::IsActive() { -#if 0 - return fmt::format("DuckStation for {} ({}) {}", SYSTEM_STR, CPU_ARCH_STR, g_scm_tag_str); +#ifdef WITH_RAINTEGRATION + return (s_client != nullptr) || s_using_raintegration; #else - return "DuckStation"; + return (s_client != nullptr); #endif } -bool Achievements::IsActive() -{ - return s_active; -} - -bool Achievements::IsLoggedIn() -{ - return s_logged_in; -} - -bool Achievements::ChallengeModeActive() +bool Achievements::IsHardcoreModeActive() { #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) return RA_HardcoreModeIsActive() != 0; #endif - return s_challenge_mode; + return s_hardcore_mode; } -bool Achievements::LeaderboardsActive() +bool Achievements::HasActiveGame() { - return ChallengeModeActive() && g_settings.achievements_leaderboards; + return s_game_id != 0; } -bool Achievements::IsTestModeActive() +u32 Achievements::GetGameID() { - return g_settings.achievements_test_mode; + return s_game_id; } -bool Achievements::IsUnofficialTestModeActive() +bool Achievements::HasAchievementsOrLeaderboards() { - return g_settings.achievements_unofficial_test_mode; + return s_has_achievements || s_has_leaderboards; } -bool Achievements::IsRichPresenceEnabled() +bool Achievements::HasLeaderboards() { - return g_settings.achievements_rich_presence; + return s_has_leaderboards; } -bool Achievements::HasActiveGame() +bool Achievements::HasRichPresence() { - return s_game_id != 0; + return s_has_rich_presence; } -u32 Achievements::GetGameID() +const std::string& Achievements::GetGameTitle() { - return s_game_id; + return s_game_title; } -std::unique_lock Achievements::GetLock() +const std::string& Achievements::GetRichPresenceString() { - return std::unique_lock(s_achievements_mutex); + return s_rich_presence_string; } -void Achievements::Initialize() +bool Achievements::Initialize() { if (IsUsingRAIntegration()) - return; + return true; - std::unique_lock lock(s_achievements_mutex); + EnsureCacheDirectoriesExist(); + + auto lock = GetLock(); AssertMsg(g_settings.achievements_enabled, "Achievements are enabled"); + Assert(!s_client && !s_http_downloader); + + if (!CreateClient(&s_client, &s_http_downloader)) + return false; + + // Hardcore starts off. We enable it on first boot. + s_hardcore_mode = false; + + rc_client_set_event_handler(s_client, ClientEventHandler); + + rc_client_set_hardcore_enabled(s_client, s_hardcore_mode); + rc_client_set_encore_mode_enabled(s_client, g_settings.achievements_spectator_mode); + rc_client_set_unofficial_enabled(s_client, g_settings.achievements_unofficial_test_mode); + rc_client_set_spectator_mode_enabled(s_client, g_settings.achievements_spectator_mode); + + // Begin disc identification early, before the login finishes. + if (System::IsValid()) + IdentifyGame(System::GetDiscPath(), nullptr); + + std::string username = Host::GetBaseStringSettingValue("Cheevos", "Username"); + std::string api_token = Host::GetBaseStringSettingValue("Cheevos", "Token"); + if (!username.empty() && !api_token.empty()) + { + Log_InfoPrintf("Attempting login with user '%s'...", username.c_str()); + s_login_request = rc_client_begin_login_with_token(s_client, username.c_str(), api_token.c_str(), + ClientLoginWithTokenCallback, nullptr); + } + + return true; +} - s_http_downloader = Common::HTTPDownloader::Create(GetUserAgent().c_str()); - if (!s_http_downloader) +bool Achievements::CreateClient(rc_client_t** client, std::unique_ptr* http) +{ + *http = Common::HTTPDownloader::Create(GetUserAgent().c_str()); + if (!*http) { Host::ReportErrorAsync("Achievements Error", "Failed to create HTTPDownloader, cannot use achievements"); - return; + return false; } - s_active = true; - s_challenge_mode = false; - rc_runtime_init(&s_rcheevos_runtime); - EnsureCacheDirectoriesExist(); + rc_client_t* new_client = rc_client_create(ClientReadMemory, ClientServerCall); + if (!new_client) + { + Host::ReportErrorAsync("Achievements Error", "rc_client_create() failed, cannot use achievements"); + http->reset(); + return false; + } + +#ifdef _DEBUG + rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_VERBOSE, ClientMessageCallback); +#else + rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_INFO, ClientMessageCallback); +#endif - s_last_ping_time.Reset(); - s_username = Host::GetBaseStringSettingValue("Cheevos", "Username"); - s_api_token = Host::GetBaseStringSettingValue("Cheevos", "Token"); - s_logged_in = (!s_username.empty() && !s_api_token.empty()); + rc_client_set_userdata(new_client, http->get()); - if (System::IsValid()) - GameChanged(System::GetDiscPath(), nullptr); + *client = new_client; + return true; +} + +void Achievements::DestroyClient(rc_client_t** client, std::unique_ptr* http) +{ + (*http)->WaitForAllRequests(); + + rc_client_destroy(*client); + *client = nullptr; + + http->reset(); } void Achievements::UpdateSettings(const Settings& old_config) @@ -484,38 +456,40 @@ void Achievements::UpdateSettings(const Settings& old_config) if (!g_settings.achievements_enabled) { // we're done here - Shutdown(); + Shutdown(false); return; } - if (!s_active) + if (!IsActive()) { // we just got enabled Initialize(); return; } - if (g_settings.achievements_challenge_mode != old_config.achievements_challenge_mode) + if (g_settings.achievements_hardcore_mode != old_config.achievements_hardcore_mode) { // Hardcore mode can only be enabled through reset (ResetChallengeMode()). - if (s_challenge_mode && !g_settings.achievements_challenge_mode) + if (s_hardcore_mode && !g_settings.achievements_hardcore_mode) { - ResetChallengeMode(); + ResetHardcoreMode(); } - else if (!s_challenge_mode && g_settings.achievements_challenge_mode) + else if (!s_hardcore_mode && g_settings.achievements_hardcore_mode) { - ImGuiFullscreen::ShowToast( - std::string(), TRANSLATE_STR("Achievements", "Hardcore mode will be enabled on system reset."), 10.0f); + ImGuiFullscreen::ShowToast(std::string(), + TRANSLATE_STR("Achievements", "Hardcore mode will be enabled on system reset."), + Host::OSD_WARNING_DURATION); } } - // FIXME: Handle changes to various settings individually - if (g_settings.achievements_test_mode != old_config.achievements_test_mode || - g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode || - g_settings.achievements_use_first_disc_from_playlist != old_config.achievements_use_first_disc_from_playlist || - g_settings.achievements_rich_presence != old_config.achievements_rich_presence) + // These cannot be modified while a game is loaded, so just toss state and reload. + if (HasActiveGame() && + (g_settings.achievements_encore_mode != old_config.achievements_encore_mode || + g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode || + g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode || + g_settings.achievements_use_first_disc_from_playlist != old_config.achievements_use_first_disc_from_playlist)) { - Shutdown(); + Shutdown(false); Initialize(); return; } @@ -524,131 +498,787 @@ void Achievements::UpdateSettings(const Settings& old_config) EnsureCacheDirectoriesExist(); } -bool Achievements::ConfirmChallengeModeDisable(const char* trigger) +bool Achievements::Shutdown(bool allow_cancel) { #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) - return (RA_WarnDisableHardcore(trigger) != 0); + { + if (System::IsValid() && allow_cancel && !RA_ConfirmLoadNewRom(true)) + return false; + + RA_SetPaused(false); + RA_ActivateGame(0); + return true; + } #endif - // I really hope this doesn't deadlock :/ - const bool confirmed = Host::ConfirmMessage( - TRANSLATE("Achievements", "Confirm Hardcore Mode"), - fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you " - "want to disable hardcore mode? {0} will be cancelled if you select No."), - trigger)); - if (!confirmed) - return false; + if (!IsActive()) + return true; - DisableChallengeMode(); + auto lock = GetLock(); + Assert(s_client && s_http_downloader); + + ClearGameInfo(); + ClearGameHash(); + + if (s_load_game_request) + { + rc_client_abort_async(s_client, s_load_game_request); + s_load_game_request = nullptr; + } + if (s_login_request) + { + rc_client_abort_async(s_client, s_login_request); + s_login_request = nullptr; + } + + s_hardcore_mode = false; + DestroyClient(&s_client, &s_http_downloader); + + Host::OnAchievementsRefreshed(); return true; } -void Achievements::DisableChallengeMode() +void Achievements::EnsureCacheDirectoriesExist() { - if (!s_active) - return; + s_image_directory = Path::Combine(EmuFolders::Cache, "achievement_images"); -#ifdef WITH_RAINTEGRATION - if (IsUsingRAIntegration()) + if (!FileSystem::DirectoryExists(s_image_directory.c_str()) && + !FileSystem::CreateDirectory(s_image_directory.c_str(), false)) { - if (RA_HardcoreModeIsActive()) - RA_DisableHardcore(); + ReportFmtError("Failed to create cache directory '{}'", s_image_directory); + } +} + +void Achievements::ClientMessageCallback(const char* message, const rc_client_t* client) +{ + Log_DevPrint(message); +} + +uint32_t Achievements::ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + switch (num_bytes) + { + case 1: + { + return CPU::SafeReadMemoryByte(address, buffer) ? 1 : 0; + } + + case 2: + { + return CPU::SafeReadMemoryHalfWord(address, reinterpret_cast(buffer)) ? 2 : 0; + } + + case 4: + { + return CPU::SafeReadMemoryWord(address, reinterpret_cast(buffer)) ? 4 : 0; + } + + default: + return 0; + } +} + +void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, + void* callback_data, rc_client_t* client) +{ + Common::HTTPDownloader::Request::Callback hd_callback = + [callback, callback_data](s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data) { + rc_api_server_response_t rr; + rr.http_status_code = status_code; + rr.body_length = data.size(); + rr.body = reinterpret_cast(data.data()); + + callback(&rr, callback_data); + }; + Common::HTTPDownloader* http = static_cast(rc_client_get_userdata(client)); + + // TODO: Content-type for post + if (request->post_data) + { + // const auto pd = std::string_view(request->post_data); + // Log_DevPrint(fmt::format("Server POST: {}", pd.substr(0, std::min(pd.length(), 10))).c_str()); + http->CreatePostRequest(request->url, request->post_data, std::move(hd_callback)); + } + else + { + http->CreateRequest(request->url, std::move(hd_callback)); + } +} + +void Achievements::IdleUpdate() +{ + if (!IsActive()) + return; + +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + return; +#endif + + const auto lock = GetLock(); + + s_http_downloader->PollRequests(); + rc_client_idle(s_client); +} + +void Achievements::FrameUpdate() +{ + if (!IsActive()) + return; + +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + RA_DoAchievementsFrame(); return; } #endif - if (s_challenge_mode) - SetChallengeMode(false); + auto lock = GetLock(); + + s_http_downloader->PollRequests(); + rc_client_do_frame(s_client); + + UpdateRichPresence(lock); } -bool Achievements::ResetChallengeMode() +void Achievements::ClientEventHandler(const rc_client_event_t* event, rc_client_t* client) { - if (!s_active || s_challenge_mode == g_settings.achievements_challenge_mode) - return false; + switch (event->type) + { + case RC_CLIENT_EVENT_RESET: + HandleResetEvent(event); + break; - SetChallengeMode(g_settings.achievements_challenge_mode); - return true; + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + HandleUnlockEvent(event); + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + HandleGameCompleteEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + HandleLeaderboardStartedEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + HandleLeaderboardFailedEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + HandleLeaderboardSubmittedEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: + HandleLeaderboardScoreboardEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + HandleLeaderboardTrackerShowEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + HandleLeaderboardTrackerHideEvent(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + HandleLeaderboardTrackerUpdateEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + HandleAchievementChallengeIndicatorShowEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + HandleAchievementChallengeIndicatorHideEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + HandleAchievementProgressIndicatorShowEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + HandleAchievementProgressIndicatorHideEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + HandleAchievementProgressIndicatorUpdateEvent(event); + break; + + case RC_CLIENT_EVENT_SERVER_ERROR: + HandleServerErrorEvent(event); + break; + + case RC_CLIENT_EVENT_DISCONNECTED: + HandleServerDisconnectedEvent(event); + break; + + case RC_CLIENT_EVENT_RECONNECTED: + HandleServerReconnectedEvent(event); + break; + + default: + Log_ErrorPrintf("Unhandled event: %u", event->type); + break; + } } -void Achievements::SetChallengeMode(bool enabled) +void Achievements::UpdateGameSummary() { - if (enabled == s_challenge_mode) + rc_client_get_user_game_summary(s_client, &s_game_summary); +} + +void Achievements::UpdateRichPresence(std::unique_lock& lock) +{ + // Limit rich presence updates to once per second, since it could change per frame. + if (!s_has_rich_presence || !s_rich_presence_poll_time.ResetIfSecondsPassed(1.0)) return; - // new mode - s_challenge_mode = enabled; + char buffer[512]; + const size_t res = rc_client_get_rich_presence_message(s_client, buffer, std::size(buffer)); + const std::string_view sv(buffer, res); + if (s_rich_presence_string == sv) + return; - if (HasActiveGame()) + s_rich_presence_string.assign(sv); + + Log_InfoPrintf("Rich presence updated: %s", s_rich_presence_string.c_str()); + Host::OnAchievementsRefreshed(); + +#ifdef WITH_DISCORD_PRESCENCE + lock.unlock(); + System::UpdateDiscordPresence(); + lock.lock(); +#endif +} + +void Achievements::GameChanged(const std::string& path, CDImage* image) +{ + std::unique_lock lock(s_achievements_mutex); + + if (!IsActive()) + return; + + IdentifyGame(path, image); +} + +void Achievements::CancelGameLoad() +{ + Log_ErrorPrint("Cancelling game load"); + + if (s_load_game_request) { - ImGuiFullscreen::ShowToast(std::string(), - enabled ? TRANSLATE_STR("Achievements", "Hardcore mode is now enabled.") : - TRANSLATE_STR("Achievements", "Hardcore mode is now disabled."), - 10.0f); + rc_client_abort_async(s_client, s_load_game_request); + s_load_game_request = nullptr; + } + rc_client_unload_game(s_client); + ClearGameHash(); + ClearGameInfo(); + DisableHardcoreMode(); + Host::OnAchievementsRefreshed(); + +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + RAIntegration::GameChanged(); + return; + } +#endif +} + +void Achievements::IdentifyGame(const std::string& path, CDImage* image) +{ + if (s_game_path == path) + { + Log_WarningPrint("Game path is unchanged."); + return; } - if (HasActiveGame() && !IsTestModeActive()) + std::unique_ptr temp_image; + if (!path.empty() && (!image || (g_settings.achievements_use_first_disc_from_playlist && image->HasSubImages() && + image->GetCurrentSubImage() != 0))) { - // deactivate, but don't clear all achievements (getting unlocks will reactivate them) - std::unique_lock lock(s_achievements_mutex); - for (Achievement& achievement : s_achievements) + temp_image = CDImage::Open(path.c_str(), g_settings.cdrom_load_image_patches, nullptr); + image = temp_image.get(); + if (!temp_image) { - DeactivateAchievement(&achievement); - achievement.locked = true; + Log_ErrorPrintf("Failed to open temporary CD image '%s'", path.c_str()); + CancelGameLoad(); + return; } - for (Leaderboard& leaderboard : s_leaderboards) - rc_runtime_deactivate_lboard(&s_rcheevos_runtime, leaderboard.id); } - // re-grab unlocks, this will reactivate what's locked in non-hardcore mode later on - if (!s_achievements.empty()) - GetUserUnlocks(); -} + std::string game_hash; + if (image) + { + game_hash = GetGameHash(image); + if (s_game_hash == game_hash) + { + // only the path has changed - different format/save state/etc. + Log_InfoPrintf("Detected path change from '%s' to '%s'", s_game_path.c_str(), path.c_str()); + s_game_path = path; + return; + } + } + + ClearGameHash(); + s_game_path = path; + s_game_hash = std::move(game_hash); -bool Achievements::Shutdown() -{ #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) { - RA_SetPaused(false); - RA_ActivateGame(0); - return true; + RAIntegration::GameChanged(); + return; } #endif - if (!s_active) - return true; + // shouldn't have a load game request when we're not logged in. + Assert(IsLoggedIn() || !s_load_game_request); - std::unique_lock lock(s_achievements_mutex); - s_http_downloader->WaitForAllRequests(); + // bail out if we're not logged in, just save the hash + if (!IsLoggedIn()) + { + Log_InfoPrintf("Skipping load game because we're not logged in."); + return; + } + + BeginLoadGame(); +} + +void Achievements::BeginLoadGame() +{ + // cancel previous requests + if (s_load_game_request) + { + rc_client_abort_async(s_client, s_load_game_request); + s_load_game_request = nullptr; + } ClearGameInfo(); - ClearGameHash(); - std::string().swap(s_username); - std::string().swap(s_api_token); - s_logged_in = false; Host::OnAchievementsRefreshed(); - s_active = false; - s_challenge_mode = false; - rc_runtime_destroy(&s_rcheevos_runtime); + if (s_game_hash.empty()) + { + // when we're booting the bios, this will fail + if (!s_game_path.empty()) + { + Host::AddKeyedOSDMessage("retroachievements_disc_read_failed", + "Failed to read executable from disc. Achievements disabled.", Host::OSD_ERROR_DURATION); + } + + rc_client_unload_game(s_client); + return; + } - s_http_downloader.reset(); - return true; + s_load_game_request = rc_client_begin_load_game(s_client, s_game_hash.c_str(), ClientLoadGameCallback, nullptr); } -bool Achievements::ConfirmSystemReset() +void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) { -#ifdef WITH_RAINTEGRATION - if (IsUsingRAIntegration()) - return RA_ConfirmLoadNewRom(false); -#endif + s_load_game_request = nullptr; - return true; + if (result != RC_OK) + { + ReportFmtError("Loading game failed: {}", error_message); + SetChallengeMode(false); + return; + } + + const rc_client_game_t* info = rc_client_get_game_info(s_client); + if (!info) + { + ReportError("rc_client_get_game_info() returned NULL"); + SetChallengeMode(false); + return; + } + + // We should have matched hardcore mode state. + Assert(s_hardcore_mode == (rc_client_get_hardcore_enabled(client) != 0)); + + s_game_id = info->id; + s_game_title = info->title; + s_has_achievements = rc_client_has_achievements(client); + s_has_leaderboards = rc_client_has_leaderboards(client); + s_has_rich_presence = rc_client_has_rich_presence(client); + s_game_icon = {}; + + // ensure fullscreen UI is ready for notifications + FullscreenUI::Initialize(); + + if (const std::string_view badge_name = info->badge_name; !badge_name.empty()) + { + s_game_icon = Path::Combine(s_image_directory, fmt::format("game_{}.png", info->id)); + if (!FileSystem::FileExists(s_game_icon.c_str())) + { + char buf[512]; + if (int err = rc_client_game_get_image_url(info, buf, std::size(buf)); err == RC_OK) + { + DownloadImage(buf, s_game_icon); + } + else + { + ReportRCError(err, "rc_client_game_get_image_url() failed: "); + } + } + } + + UpdateGameSummary(); + DisplayAchievementSummary(); + + Host::OnAchievementsRefreshed(); +} + +void Achievements::ClearGameInfo() +{ + ClearUIState(); + s_active_leaderboard_trackers = {}; + s_active_challenge_indicators = {}; + s_active_progress_indicator.reset(); + s_game_id = 0; + s_game_title = {}; + s_game_icon = {}; + s_has_achievements = false; + s_has_leaderboards = false; + s_has_rich_presence = false; + s_rich_presence_string = {}; + s_game_summary = {}; +} + +void Achievements::ClearGameHash() +{ + s_game_path = {}; + std::string().swap(s_game_hash); +} + +void Achievements::DisplayAchievementSummary() +{ + if (g_settings.achievements_notifications && FullscreenUI::Initialize()) + { + std::string title; + if (IsHardcoreModeActive()) + title = fmt::format(TRANSLATE_FS("Achievements", "{} (Hardcore Mode)"), s_game_title); + else + title = s_game_title; + + std::string summary; + if (s_game_summary.num_core_achievements > 0) + { + summary = fmt::format(TRANSLATE_FS("Achievements", "You have earned {} of {} achievements, and {} of {} points."), + s_game_summary.num_unlocked_achievements, s_game_summary.num_core_achievements, + s_game_summary.points_unlocked, s_game_summary.points_core); + } + else + { + summary = TRANSLATE_STR("Achievements", "This game has no achievements."); + } + + ImGuiFullscreen::AddNotification("achievement_summary", ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, std::move(title), + std::move(summary), s_game_icon); + } + + // Technically not going through the resource API, but since we're passing this to something else, we can't. + if (g_settings.achievements_sound_effects) + PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, INFO_SOUND_NAME).c_str()); +} + +void Achievements::HandleResetEvent(const rc_client_event_t* event) +{ + // We handle system resets ourselves, but still need to reset the client's state. + Log_InfoPrintf("Resetting runtime due to reset event"); + rc_client_reset(s_client); + + if (HasActiveGame()) + UpdateGameSummary(); +} + +void Achievements::HandleUnlockEvent(const rc_client_event_t* event) +{ + const rc_client_achievement_t* cheevo = event->achievement; + DebugAssert(cheevo); + + Log_InfoPrintf("Achievement %s (%u) for game %u unlocked", cheevo->title, cheevo->id, s_game_id); + UpdateGameSummary(); + + if (g_settings.achievements_notifications && FullscreenUI::Initialize()) + { + std::string title; + if (cheevo->category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) + title = fmt::format(TRANSLATE_FS("Achievements", "{} (Unofficial)"), cheevo->title); + else + title = cheevo->title; + + std::string badge_path = GetAchievementBadgePath(cheevo, cheevo->state); + + ImGuiFullscreen::AddNotification(fmt::format("achievement_unlock_{}", cheevo->id), + g_settings.achievements_notification_duration, std::move(title), + cheevo->description, std::move(badge_path)); + } + + if (g_settings.achievements_sound_effects) + PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, UNLOCK_SOUND_NAME).c_str()); +} + +void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event) +{ + Log_InfoPrintf("Game %u complete", s_game_id); + UpdateGameSummary(); + + if (g_settings.achievements_notifications && FullscreenUI::Initialize()) + { + std::string title = fmt::format(TRANSLATE_FS("Achievements", "Mastered {}"), s_game_title); + std::string message = fmt::format(TRANSLATE_FS("Achievements", "{} achievements, {} points"), + s_game_summary.num_unlocked_achievements, s_game_summary.points_unlocked); + + ImGuiFullscreen::AddNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, std::move(title), + std::move(message), s_game_icon); + } +} + +void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Leaderboard %u (%s) started", event->leaderboard->id, event->leaderboard->title); + + if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) + { + std::string title = event->leaderboard->title; + std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt started."); + + ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), + LEADERBOARD_STARTED_NOTIFICATION_TIME, std::move(title), std::move(message), + s_game_icon); + } +} + +void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Leaderboard %u (%s) failed", event->leaderboard->id, event->leaderboard->title); + + if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) + { + std::string title = event->leaderboard->title; + std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt failed."); + + ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), + LEADERBOARD_FAILED_NOTIFICATION_TIME, std::move(title), std::move(message), + s_game_icon); + } +} + +void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Leaderboard %u (%s) submitted", event->leaderboard->id, event->leaderboard->title); + + if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) + { + static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + TRANSLATE_NOOP("Achievements", "Your Time: {} (Submitting)"), + TRANSLATE_NOOP("Achievements", "Your Score: {} (Submitting)"), + TRANSLATE_NOOP("Achievements", "Your Value: {} (Submitting)"), + }; + + std::string title = event->leaderboard->title; + std::string message = + fmt::format(fmt::runtime(Host::TranslateToStringView( + "Achievements", + value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), + event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown"); + + ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), + g_settings.achievements_leaderboard_duration, std::move(title), std::move(message), + s_game_icon); + } + + if (g_settings.achievements_sound_effects) + PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, LBSUBMIT_SOUND_NAME).c_str()); +} + +void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Leaderboard %u scoreboard rank %u of %u", event->leaderboard_scoreboard->leaderboard_id, + event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries); + + if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize()) + { + static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + TRANSLATE_NOOP("Achievements", "Your Time: {} (Best: {})"), + TRANSLATE_NOOP("Achievements", "Your Score: {} (Best: {})"), + TRANSLATE_NOOP("Achievements", "Your Value: {} (Best: {})"), + }; + + std::string title = event->leaderboard->title; + std::string message = fmt::format( + TRANSLATE_FS("Achievements", "{}\nLeaderboard Position: {} of {}"), + fmt::format(fmt::runtime(Host::TranslateToStringView( + "Achievements", + value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), + event->leaderboard_scoreboard->submitted_score, event->leaderboard_scoreboard->best_score), + event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries); + + ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id), + g_settings.achievements_leaderboard_duration, std::move(title), std::move(message), + s_game_icon); + } +} + +void Achievements::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Showing leaderboard tracker: %u: %s", event->leaderboard_tracker->id, + event->leaderboard_tracker->display); + + TinyString width_string; + width_string.AppendString(ICON_FA_STOPWATCH); + const u32 display_len = static_cast(std::strlen(event->leaderboard_tracker->display)); + for (u32 i = 0; i < display_len; i++) + width_string.AppendCharacter('0'); + + LeaderboardTrackerIndicator indicator; + indicator.tracker_id = event->leaderboard_tracker->id; + indicator.size = ImGuiFullscreen::g_medium_font->CalcTextSizeA( + ImGuiFullscreen::g_medium_font->FontSize, FLT_MAX, 0.0f, width_string.GetCharArray(), + width_string.GetCharArray() + width_string.GetLength()); + indicator.text = fmt::format(ICON_FA_STOPWATCH " {}", event->leaderboard_tracker->display); + indicator.active = true; + s_active_leaderboard_trackers.push_back(std::move(indicator)); } -void Achievements::ResetRuntime() +void Achievements::HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event) +{ + const u32 id = event->leaderboard_tracker->id; + auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(), + [id](const auto& it) { return it.tracker_id == id; }); + if (it == s_active_leaderboard_trackers.end()) + return; + + Log_DevPrintf("Hiding leaderboard tracker: %u", id); + it->active = false; + it->show_hide_time.Reset(); +} + +void Achievements::HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event) +{ + const u32 id = event->leaderboard_tracker->id; + auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(), + [id](const auto& it) { return it.tracker_id == id; }); + if (it == s_active_leaderboard_trackers.end()) + return; + + Log_DevPrintf("Updating leaderboard tracker: %u: %s", event->leaderboard_tracker->id, + event->leaderboard_tracker->display); + + it->text.clear(); + fmt::format_to(std::back_inserter(it->text), ICON_FA_STOPWATCH " {}", event->leaderboard_tracker->display); + it->active = true; +} + +void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event) +{ + if (auto it = + std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(), + [event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; }); + it != s_active_challenge_indicators.end()) + { + it->show_hide_time.Reset(); + it->active = true; + return; + } + + AchievementChallengeIndicator indicator; + indicator.achievement = event->achievement; + indicator.badge_path = GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + indicator.active = true; + s_active_challenge_indicators.push_back(std::move(indicator)); + + Log_DevPrintf("Show challenge indicator for %u (%s)", event->achievement->id, event->achievement->title); +} + +void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event) +{ + auto it = + std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(), + [event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; }); + if (it == s_active_challenge_indicators.end()) + return; + + Log_DevPrintf("Hide challenge indicator for %u (%s)", event->achievement->id, event->achievement->title); + it->show_hide_time.Reset(); + it->active = false; +} + +void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Showing progress indicator: %u (%s): %s", event->achievement->id, event->achievement->title, + event->achievement->measured_progress); + + if (!s_active_progress_indicator.has_value()) + s_active_progress_indicator.emplace(); + else + s_active_progress_indicator->show_hide_time.Reset(); + + s_active_progress_indicator->achievement = event->achievement; + s_active_progress_indicator->badge_path = + GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + s_active_progress_indicator->active = true; +} + +void Achievements::HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event) +{ + if (!s_active_progress_indicator.has_value()) + return; + + Log_DevPrintf("Hiding progress indicator"); + s_active_progress_indicator->show_hide_time.Reset(); + s_active_progress_indicator->active = false; +} + +void Achievements::HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event) +{ + Log_DevPrintf("Updating progress indicator: %u (%s): %s", event->achievement->id, event->achievement->title, + event->achievement->measured_progress); + s_active_progress_indicator->achievement = event->achievement; + s_active_progress_indicator->active = true; +} + +void Achievements::HandleServerErrorEvent(const rc_client_event_t* event) +{ + std::string message = + fmt::format(TRANSLATE_FS("Achievements", "Server error in {}:\n{}"), + event->server_error->api ? event->server_error->api : "UNKNOWN", + event->server_error->error_message ? event->server_error->error_message : "UNKNOWN"); + Log_ErrorPrint(message.c_str()); + Host::AddOSDMessage(std::move(message), Host::OSD_ERROR_DURATION); +} + +void Achievements::HandleServerDisconnectedEvent(const rc_client_event_t* event) +{ + Log_WarningPrintf("Server disconnected."); + + if (FullscreenUI::Initialize()) + { + ImGuiFullscreen::ShowToast( + TRANSLATE_STR("Achievements", "Achievements Disconnected"), + TRANSLATE_STR("Achievements", + "An unlock request could not be completed. We will keep retrying to submit this request."), + Host::OSD_ERROR_DURATION); + } +} + +void Achievements::HandleServerReconnectedEvent(const rc_client_event_t* event) +{ + Log_WarningPrintf("Server reconnected."); + + if (FullscreenUI::Initialize()) + { + ImGuiFullscreen::ShowToast(TRANSLATE_STR("Achievements", "Achievements Reconnected"), + TRANSLATE_STR("Achievements", "All pending unlock requests have completed."), + Host::OSD_INFO_DURATION); + } +} + +void Achievements::ResetClient() { #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) @@ -658,12 +1288,11 @@ void Achievements::ResetRuntime() } #endif - if (!s_active) + if (!IsActive()) return; - std::unique_lock lock(s_achievements_mutex); - Log_DevPrint("Resetting rcheevos state..."); - rc_runtime_reset(&s_rcheevos_runtime); + Log_DevPrint("Reset client"); + rc_client_reset(s_client); } void Achievements::OnSystemPaused(bool paused) @@ -674,7 +1303,7 @@ void Achievements::OnSystemPaused(bool paused) #endif } -void Achievements::FrameUpdate() +void Achievements::DisableHardcoreMode() { if (!IsActive()) return; @@ -682,46 +1311,65 @@ void Achievements::FrameUpdate() #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) { - RA_DoAchievementsFrame(); + if (RA_HardcoreModeIsActive()) + RA_DisableHardcore(); + return; } #endif - s_http_downloader->PollRequests(); - - if (HasActiveGame()) - { - std::unique_lock lock(s_achievements_mutex); - rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &PeekMemory, nullptr, nullptr); - UpdateRichPresence(); - } + if (s_hardcore_mode) + SetChallengeMode(false); } -void Achievements::ProcessPendingHTTPRequests() +bool Achievements::ResetHardcoreMode() { -#ifdef WITH_RAINTEGRATION - if (IsUsingRAIntegration()) - return; -#endif + if (!IsActive()) + return false; - if (!s_http_downloader) + const auto lock = GetLock(); + + // If we're not logged in, don't apply hardcore mode restrictions. + // If we later log in, we'll start with it off anyway. + const bool wanted_hardcore_mode = (IsLoggedIn() || s_load_game_request) && g_settings.achievements_hardcore_mode; + if (s_hardcore_mode == wanted_hardcore_mode) + return false; + + SetChallengeMode(wanted_hardcore_mode); + return true; +} + +void Achievements::SetChallengeMode(bool enabled) +{ + if (enabled == s_hardcore_mode) return; - s_http_downloader->PollRequests(); + // new mode + s_hardcore_mode = enabled; - if (HasActiveGame() && !IsTestModeActive()) + if (HasActiveGame()) { - const s32 ping_frequency = - g_settings.achievements_rich_presence ? RICH_PRESENCE_PING_FREQUENCY : NO_RICH_PRESENCE_PING_FREQUENCY; - if (static_cast(s_last_ping_time.GetTimeSeconds()) >= ping_frequency) - SendPing(); + ImGuiFullscreen::ShowToast(std::string(), + enabled ? TRANSLATE_STR("Achievements", "Hardcore mode is now enabled.") : + TRANSLATE_STR("Achievements", "Hardcore mode is now disabled."), + Host::OSD_INFO_DURATION); } + + rc_client_set_hardcore_enabled(s_client, enabled); + DebugAssert((rc_client_get_hardcore_enabled(s_client) != 0) == enabled); + if (HasActiveGame()) + UpdateGameSummary(); + + // Toss away UI state, because it's invalid now + ClearUIState(); + + Host::OnAchievementsHardcoreModeChanged(); } bool Achievements::DoState(StateWrapper& sw) { // if we're inactive, we still need to skip the data (if any) - if (!s_active) + if (!IsActive()) { u32 data_size = 0; sw.Do(&data_size); @@ -737,7 +1385,7 @@ bool Achievements::DoState(StateWrapper& sw) { // if we're active, make sure we've downloaded and activated all the achievements // before deserializing, otherwise that state's going to get lost. - if (!IsUsingRAIntegration() && s_http_downloader->HasAnyRequests()) + if (!IsUsingRAIntegration() && s_load_game_request) { Host::DisplayLoadingScreen("Downloading achievements data..."); s_http_downloader->WaitForAllRequests(); @@ -753,9 +1401,9 @@ bool Achievements::DoState(StateWrapper& sw) if (IsUsingRAIntegration()) RA_OnReset(); else - rc_runtime_reset(&s_rcheevos_runtime); + rc_client_reset(s_client); #else - rc_runtime_reset(&s_rcheevos_runtime); + rc_client_reset(s_client); #endif return !sw.HasError(); @@ -773,11 +1421,11 @@ bool Achievements::DoState(StateWrapper& sw) } else { - const int result = rc_runtime_deserialize_progress(&s_rcheevos_runtime, data.get(), nullptr); + const int result = rc_client_deserialize_progress(s_client, data.get()); if (result != RC_OK) { Log_WarningPrintf("Failed to deserialize cheevos state (%d), resetting", result); - rc_runtime_reset(&s_rcheevos_runtime); + rc_client_reset(s_client); } } #endif @@ -808,12 +1456,12 @@ bool Achievements::DoState(StateWrapper& sw) #endif { // internally this happens twice.. not great. - const int size = rc_runtime_progress_size(&s_rcheevos_runtime, nullptr); + const u32 size = static_cast(rc_client_progress_size(s_client)); data_size = (size >= 0) ? static_cast(size) : 0; data = std::unique_ptr(new u8[data_size]); - const int result = rc_runtime_serialize_progress(data.get(), &s_rcheevos_runtime, nullptr); + const int result = rc_client_serialize_progress(s_client, data.get()); if (result != RC_OK) { // set data to zero, effectively serializing nothing @@ -830,1202 +1478,1366 @@ bool Achievements::DoState(StateWrapper& sw) } } -bool Achievements::SafeHasAchievementsOrLeaderboards() +std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* achievement, int state) { - std::unique_lock lock(s_achievements_mutex); - return !s_achievements.empty() || s_leaderboards.empty(); + static constexpr std::array s_achievement_state_strings = { + {"inactive", "active", "unlocked", "disabled"}}; + + std::string path; + + if (achievement->badge_name[0] == 0) + return path; + + path = Path::Combine(s_image_directory, TinyString::FromFmt("achievement_{}_{}_{}.png", s_game_id, achievement->id, + s_achievement_state_strings[state])); + + if (!FileSystem::FileExists(path.c_str())) + { + char buf[512]; + const int res = rc_client_achievement_get_image_url(achievement, state, buf, std::size(buf)); + if (res == RC_OK) + DownloadImage(buf, path); + else + ReportRCError(res, "rc_client_achievement_get_image_url() for {} failed", achievement->title); + } + + return path; } -const std::string& Achievements::GetUsername() +std::string Achievements::GetUserBadgePath(const std::string_view& username) { - return s_username; + // definitely want to sanitize usernames... :) + std::string path; + const std::string clean_username = Path::SanitizeFileName(username); + if (!clean_username.empty()) + path = Path::Combine(s_image_directory, TinyString::FromFmt("user_{}.png", clean_username)); + return path; } -const std::string& Achievements::GetRichPresenceString() +std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry) { - return s_rich_presence_string; + // TODO: maybe we should just cache these in memory... + std::string path = GetUserBadgePath(entry->user); + + if (!FileSystem::FileExists(path.c_str())) + { + char buf[512]; + const int res = rc_client_leaderboard_entry_get_user_image_url(entry, buf, std::size(buf)); + if (res == RC_OK) + DownloadImage(buf, path); + else + ReportRCError(res, "rc_client_leaderboard_entry_get_user_image_url() for {} failed", entry->user); + } + + return path; } -void Achievements::EnsureCacheDirectoriesExist() +bool Achievements::IsLoggedIn() { - s_game_icon_cache_directory = Path::Combine(EmuFolders::Cache, "achievement_gameicon"); - s_achievement_icon_cache_directory = Path::Combine(EmuFolders::Cache, "achievement_badge"); + return (rc_client_get_user_info(s_client) != nullptr); +} - if (!FileSystem::DirectoryExists(s_game_icon_cache_directory.c_str()) && - !FileSystem::CreateDirectory(s_game_icon_cache_directory.c_str(), false)) +bool Achievements::Login(const char* username, const char* password, Error* error) +{ + auto lock = GetLock(); + + // We need to use a temporary client if achievements aren't currently active. + rc_client_t* client = s_client; + Common::HTTPDownloader* http = s_http_downloader.get(); + const bool is_temporary_client = (client == nullptr); + std::unique_ptr temporary_downloader; + ScopedGuard temporary_client_guard = [&client, is_temporary_client, &temporary_downloader]() { + if (is_temporary_client) + DestroyClient(&client, &temporary_downloader); + }; + if (is_temporary_client) { - FormattedError("Failed to create cache directory '%s'", s_game_icon_cache_directory.c_str()); + if (!CreateClient(&client, &temporary_downloader)) + { + Error::SetString(error, "Failed to create client."); + return false; + } + http = temporary_downloader.get(); } - if (!FileSystem::DirectoryExists(s_achievement_icon_cache_directory.c_str()) && - !FileSystem::CreateDirectory(s_achievement_icon_cache_directory.c_str(), false)) + LoginWithPasswordParameters params = {username, error, nullptr, false}; + + params.request = + rc_client_begin_login_with_password(client, username, password, ClientLoginWithPasswordCallback, ¶ms); + if (!params.request) { - FormattedError("Failed to create cache directory '%s'", s_achievement_icon_cache_directory.c_str()); + Error::SetString(error, "Failed to create login request."); + return false; } + + // Wait until the login request completes. + http->WaitForAllRequests(); + Assert(!params.request); + + // Success? Assume the callback set the error message. + if (!params.result) + return false; + + // If we were't a temporary client, get the game loaded. + if (System::IsValid() && !is_temporary_client) + BeginLoadGame(); + + return true; } -void Achievements::LoginCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data) +void Achievements::ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, + void* userdata) { - std::unique_lock lock(s_achievements_mutex); + Assert(userdata); + + LoginWithPasswordParameters* params = static_cast(userdata); + params->request = nullptr; - RAPIResponse response( - status_code, data); - if (!response || !response.username || !response.api_token) + if (result != RC_OK) { - FormattedError("Login failed. Please check your user name and password, and try again."); + Log_ErrorPrintf("Login failed: %s: %s", rc_error_str(result), error_message ? error_message : "Unknown"); + Error::SetString(params->error, + fmt::format("{}: {}", rc_error_str(result), error_message ? error_message : "Unknown")); + params->result = false; return; } - std::string username(response.username); - std::string api_token(response.api_token); - - // save to config - Host::SetBaseStringSettingValue("Cheevos", "Username", username.c_str()); - Host::SetBaseStringSettingValue("Cheevos", "Token", api_token.c_str()); - Host::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str()); - Host::CommitBaseSettingChanges(); - - if (s_active) + // Grab the token from the client, and save it to the config. + const rc_client_user_t* user = rc_client_get_user_info(client); + if (!user || !user->token) { - s_username = std::move(username); - s_api_token = std::move(api_token); - s_logged_in = true; - - // If we have a game running, set it up. - if (!s_game_hash.empty()) - SendGetGameId(); + Log_ErrorPrint("rc_client_get_user_info() returned NULL"); + Error::SetString(params->error, "rc_client_get_user_info() returned NULL"); + params->result = false; + return; } -} -void Achievements::LoginASyncCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) -{ - ImGuiFullscreen::CloseBackgroundProgressDialog("cheevos_async_login"); + params->result = true; - LoginCallback(status_code, std::move(content_type), std::move(data)); -} + // Store configuration. + Host::SetBaseStringSettingValue("Cheevos", "Username", params->username); + Host::SetBaseStringSettingValue("Cheevos", "Token", user->token); + Host::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str()); + Host::CommitBaseSettingChanges(); -void Achievements::SendLogin(const char* username, const char* password, Common::HTTPDownloader* http_downloader, - Common::HTTPDownloader::Request::Callback callback) -{ - RAPIRequest request; - request.username = username; - request.password = password; - request.api_token = nullptr; - request.Send(http_downloader, std::move(callback)); + ShowLoginSuccess(client); } -bool Achievements::LoginAsync(const char* username, const char* password) +void Achievements::ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, + void* userdata) { - s_http_downloader->WaitForAllRequests(); + s_login_request = nullptr; - if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration()) - return false; + if (result != RC_OK) + { + ReportFmtError("Login failed: {}", error_message); + Host::OnAchievementsLoginRequested(LoginRequestReason::TokenInvalid); + return; + } - if (FullscreenUI::IsInitialized()) - ImGuiFullscreen::OpenBackgroundProgressDialog("cheevos_async_login", "Logging in to RetroAchivements...", 0, 0, 0); + ShowLoginSuccess(client); - SendLogin(username, password, s_http_downloader.get(), LoginASyncCallback); - return true; + if (System::IsValid()) + BeginLoadGame(); } -bool Achievements::Login(const char* username, const char* password) +void Achievements::ShowLoginSuccess(const rc_client_t* client) { - if (s_active) - s_http_downloader->WaitForAllRequests(); + const rc_client_user_t* user = rc_client_get_user_info(client); + if (!user) + return; - if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration()) - return false; + Host::OnAchievementsLoginSuccess(user->username, user->score, user->score_softcore, user->num_unread_messages); - if (s_active) + if (System::IsValid()) { - SendLogin(username, password, s_http_downloader.get(), LoginCallback); - s_http_downloader->WaitForAllRequests(); - return IsLoggedIn(); + const auto lock = GetLock(); + if (s_client == client) + Host::RunOnCPUThread(ShowLoginNotification); } +} - // create a temporary downloader if we're not initialized - AssertMsg(!s_active, "RetroAchievements is not active on login"); - std::unique_ptr http_downloader = Common::HTTPDownloader::Create(GetUserAgent().c_str()); - if (!http_downloader) - return false; +void Achievements::ShowLoginNotification() +{ + const rc_client_user_t* user = rc_client_get_user_info(s_client); + if (!user) + return; + + if (g_settings.achievements_notifications && FullscreenUI::Initialize()) + { + std::string badge_path = GetUserBadgePath(user->username); + if (!FileSystem::FileExists(badge_path.c_str())) + { + char url[512]; + const int res = rc_client_user_get_image_url(user, url, std::size(url)); + if (res == RC_OK) + DownloadImage(url, badge_path); + else + ReportRCError(res, "rc_client_user_get_image_url() failed: "); + } - SendLogin(username, password, http_downloader.get(), LoginCallback); - http_downloader->WaitForAllRequests(); + //: Summary for login notification. + std::string title = user->display_name; + std::string summary = fmt::format(TRANSLATE_FS("Achievements", "Score: {} ({} softcore)\nUnread messages: {}"), + user->score, user->score_softcore, user->num_unread_messages); - return !Host::GetBaseStringSettingValue("Cheevos", "Token").empty(); + ImGuiFullscreen::AddNotification("achievements_login", LOGIN_NOTIFICATION_TIME, std::move(title), + std::move(summary), std::move(badge_path)); + } } void Achievements::Logout() { - if (s_active) + if (IsActive()) { - std::unique_lock lock(s_achievements_mutex); - s_http_downloader->WaitForAllRequests(); - if (s_logged_in) + const auto lock = GetLock(); + + if (HasActiveGame()) { ClearGameInfo(); - std::string().swap(s_username); - std::string().swap(s_api_token); - s_logged_in = false; + ClearGameHash(); Host::OnAchievementsRefreshed(); } + + Log_InfoPrint("Logging out..."); + rc_client_logout(s_client); } - // remove from config + Log_InfoPrint("Clearing credentials..."); Host::DeleteBaseSettingValue("Cheevos", "Username"); Host::DeleteBaseSettingValue("Cheevos", "Token"); Host::DeleteBaseSettingValue("Cheevos", "LoginTimestamp"); Host::CommitBaseSettingChanges(); } -void Achievements::DownloadImage(std::string url, std::string cache_filename) -{ - auto callback = [cache_filename](s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) { - if (status_code != HTTP_OK) - return; - - if (!FileSystem::WriteBinaryFile(cache_filename.c_str(), data.data(), data.size())) - { - Log_ErrorPrintf("Failed to write badge image to '%s'", cache_filename.c_str()); - return; - } - - ImGuiFullscreen::InvalidateCachedTexture(cache_filename); - }; - - s_http_downloader->CreateRequest(std::move(url), std::move(callback)); -} - -void Achievements::DisplayAchievementSummary() +bool Achievements::ConfirmSystemReset() { - std::string title; - if (ChallengeModeActive()) - title = fmt::format(TRANSLATE_FS("Achievements", "{} (Hardcore Mode)"), s_game_title); - else - title = s_game_title; - - std::string summary; - if (GetAchievementCount() > 0) - { - summary = fmt::format(TRANSLATE_FS("Achievements", "You have earned {} of {} achievements, and {} of {} points."), - GetUnlockedAchiementCount(), GetAchievementCount(), GetCurrentPointsForGame(), - GetMaximumPointsForGame()); - } - else - { - summary = TRANSLATE_STR("Achievements", "This game has no achievements."); - } - if (GetLeaderboardCount() > 0) - { - summary.push_back('\n'); - if (LeaderboardsActive()) - summary.append("Leaderboard submission is enabled."); - } - - Host::RunOnCPUThread([title = std::move(title), summary = std::move(summary), icon = s_game_icon]() { - if (FullscreenUI::IsInitialized() && g_settings.achievements_notifications) - ImGuiFullscreen::AddNotification(10.0f, std::move(title), std::move(summary), std::move(icon)); +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + return RA_ConfirmLoadNewRom(false); +#endif - // Technically not going through the resource API, but since we're passing this to something else, we can't. - if (g_settings.achievements_sound_effects) - PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, INFO_SOUND_NAME).c_str()); - }); + return true; } -void Achievements::DisplayMasteredNotification() +bool Achievements::ConfirmHardcoreModeDisable(const char* trigger) { - if (!FullscreenUI::IsInitialized() || !g_settings.achievements_notifications) - return; +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + return (RA_WarnDisableHardcore(trigger) != 0); +#endif - std::string title(fmt::format("Mastered {}", s_game_title)); - std::string message(fmt::format("{} achievements, {} points", GetAchievementCount(), GetCurrentPointsForGame())); + // I really hope this doesn't deadlock :/ + const bool confirmed = Host::ConfirmMessage( + TRANSLATE("Achievements", "Confirm Hardcore Mode"), + fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you " + "want to disable hardcore mode? {0} will be cancelled if you select No."), + trigger)); + if (!confirmed) + return false; - ImGuiFullscreen::AddNotification(20.0f, std::move(title), std::move(message), s_game_icon); + DisableHardcoreMode(); + return true; } -void Achievements::GetUserUnlocksCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) +void Achievements::ClearUIState() { - if (!System::IsValid()) - return; + if (FullscreenUI::IsAchievementsWindowOpen() || FullscreenUI::IsLeaderboardsWindowOpen()) + FullscreenUI::ReturnToMainWindow(); - RAPIResponse - response(status_code, data); + s_achievement_badge_paths = {}; - std::unique_lock lock(s_achievements_mutex); - if (!response) + CloseLeaderboard(); + s_leaderboard_user_icon_paths = {}; + s_leaderboard_entry_lists = {}; + if (s_leaderboard_list) { - ClearGameInfo(true, false); - return; + rc_client_destroy_leaderboard_list(s_leaderboard_list); + s_leaderboard_list = nullptr; } - // flag achievements as unlocked - for (u32 i = 0; i < response.num_achievement_ids; i++) + if (s_achievement_list) { - Achievement* cheevo = GetMutableAchievementByID(response.achievement_ids[i]); - if (!cheevo) - { - Log_ErrorPrintf("Server returned unknown achievement %u", response.achievement_ids[i]); - continue; - } - - cheevo->locked = false; + rc_client_destroy_achievement_list(s_achievement_list); + s_achievement_list = nullptr; } - - // start scanning for locked achievements - ActivateLockedAchievements(); - DisplayAchievementSummary(); - SendPlaying(); - UpdateRichPresence(); - SendPing(); - Host::OnAchievementsRefreshed(); } -void Achievements::GetUserUnlocks() +template +static float IndicatorOpacity(const T& i) { - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_id = s_game_id; - request.hardcore = static_cast(ChallengeModeActive()); - request.Send(GetUserUnlocksCallback); + const float elapsed = static_cast(i.show_hide_time.GetTimeSeconds()); + const float time = i.active ? Achievements::INDICATOR_FADE_IN_TIME : Achievements::INDICATOR_FADE_OUT_TIME; + const float opacity = (elapsed >= time) ? 1.0f : (elapsed / time); + return (i.active) ? opacity : (1.0f - opacity); } -void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) +void Achievements::DrawGameOverlays() { - if (!System::IsValid()) - return; - - RAPIResponse - response(status_code, data); + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; - std::unique_lock lock(s_achievements_mutex); - ClearGameInfo(); - if (!response || !response.title) - { - DisableChallengeMode(); + if (!HasActiveGame() || !g_settings.achievements_overlays) return; - } - // ensure fullscreen UI is ready - Host::RunOnCPUThread(FullscreenUI::Initialize); + const auto lock = GetLock(); - s_game_id = response.id; - s_game_title = response.title; - - // try for a icon - if (response.image_name && std::strlen(response.image_name) > 0) - { - s_game_icon = Path::Combine(s_game_icon_cache_directory, fmt::format("{}.png", s_game_id)); - if (!FileSystem::FileExists(s_game_icon.c_str())) - { - RAPIRequest request; - request.image_name = response.image_name; - request.image_type = RC_IMAGE_TYPE_GAME; - request.DownloadImage(s_game_icon); - } - } + const float spacing = LayoutScale(10.0f); + const float padding = LayoutScale(10.0f); + const ImVec2 image_size = + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT); + const ImGuiIO& io = ImGui::GetIO(); + ImVec2 position = ImVec2(io.DisplaySize.x - padding, io.DisplaySize.y - padding); + ImDrawList* dl = ImGui::GetBackgroundDrawList(); - // parse achievements - for (u32 i = 0; i < response.num_achievements; i++) + if (!s_active_challenge_indicators.empty()) { - const rc_api_achievement_definition_t& defn = response.achievements[i]; + const float x_advance = image_size.x + spacing; + ImVec2 current_position = ImVec2(position.x - image_size.x, position.y - image_size.y); - // Skip local and unofficial achievements for now, unless "Test Unofficial Achievements" is enabled - if (defn.category == RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL) + for (auto it = s_active_challenge_indicators.begin(); it != s_active_challenge_indicators.end();) { - if (!IsUnofficialTestModeActive()) + const AchievementChallengeIndicator& indicator = *it; + const float opacity = IndicatorOpacity(indicator); + const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); + + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); + if (badge) { - Log_WarningPrintf("Skipping unofficial achievement %u (%s)", defn.id, defn.title); - continue; + dl->AddImage(badge, current_position, current_position + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + col); + current_position.x -= x_advance; } - } - // local achievements shouldn't be in this list, but just in case? - else if (defn.category != RC_ACHIEVEMENT_CATEGORY_CORE) - { - continue; - } - - if (GetMutableAchievementByID(defn.id)) - { - Log_ErrorPrintf("Achievement %u already exists", defn.id); - continue; - } - if (!defn.definition || !defn.title || !defn.description || !defn.badge_name) - { - Log_ErrorPrintf("Incomplete achievement %u", defn.id); - continue; + if (!indicator.active && opacity <= 0.01f) + { + Log_DevPrintf("Remove challenge indicator"); + it = s_active_challenge_indicators.erase(it); + } + else + { + ++it; + } } - Achievement cheevo; - cheevo.id = defn.id; - cheevo.memaddr = defn.definition; - cheevo.title = defn.title; - cheevo.description = defn.description; - cheevo.badge_name = defn.badge_name; - cheevo.locked = true; - cheevo.active = false; - cheevo.primed = false; - cheevo.points = defn.points; - cheevo.category = static_cast(defn.category); - s_achievements.push_back(std::move(cheevo)); + position.y -= image_size.y + padding; } - for (u32 i = 0; i < response.num_leaderboards; i++) + if (s_active_progress_indicator.has_value()) { - const rc_api_leaderboard_definition_t& defn = response.leaderboards[i]; - if (!defn.title || !defn.description || !defn.definition) - { - Log_ErrorPrintf("Incomplete achievement %u", defn.id); - continue; - } + const AchievementProgressIndicator& indicator = s_active_progress_indicator.value(); + const float opacity = IndicatorOpacity(indicator); + const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); + + const char* text_start = s_active_progress_indicator->achievement->measured_progress; + const char* text_end = text_start + std::strlen(text_start); + const ImVec2 text_size = g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text_start, text_end); + + const ImVec2 box_min = ImVec2(position.x - image_size.x - text_size.x - spacing - padding * 2.0f, + position.y - image_size.y - padding * 2.0f); + const ImVec2 box_max = position; + const float box_rounding = LayoutScale(1.0f); - Leaderboard lboard; - lboard.id = defn.id; - lboard.title = defn.title; - lboard.description = defn.description; - lboard.format = defn.format; - s_leaderboards.push_back(std::move(lboard)); + dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)), box_rounding); + dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding); - const int err = rc_runtime_activate_lboard(&s_rcheevos_runtime, defn.id, defn.definition, nullptr, 0); - if (err != RC_OK) + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); + if (badge) { - Log_ErrorPrintf("Leaderboard %u memaddr parse error: %s", defn.id, rc_error_str(err)); + const ImVec2 badge_pos = box_min + ImVec2(padding, padding); + dl->AddImage(badge, badge_pos, badge_pos + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), col); } - else + + const ImVec2 text_pos = + box_min + ImVec2(padding + image_size.x + spacing, (box_max.y - box_min.y - text_size.y) * 0.5f); + const ImVec4 text_clip_rect(text_pos.x, text_pos.y, box_max.x, box_max.y); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, col, text_start, text_end, 0.0f, &text_clip_rect); + + if (!indicator.active && opacity <= 0.01f) { - Log_DevPrintf("Activated leaderboard %s (%u)", defn.title, defn.id); + Log_DevPrintf("Remove progress indicator"); + s_active_progress_indicator.reset(); } } - // parse rich presence - if (response.rich_presence_script && std::strlen(response.rich_presence_script) > 0) - { - const int res = rc_runtime_activate_richpresence(&s_rcheevos_runtime, response.rich_presence_script, nullptr, 0); - if (res == RC_OK) - s_has_rich_presence = true; - else - Log_WarningPrintf("Failed to activate rich presence: %s", rc_error_str(res)); - } - - Log_InfoPrintf("Game Title: %s", s_game_title.c_str()); - Log_InfoPrintf("Achievements: %zu", s_achievements.size()); - Log_InfoPrintf("Leaderboards: %zu", s_leaderboards.size()); - - // We don't want to block saving/loading states when there's no achievements. - if (s_achievements.empty() && s_leaderboards.empty()) - DisableChallengeMode(); - - if (!s_achievements.empty() || s_has_rich_presence) + if (!s_active_leaderboard_trackers.empty()) { - if (!IsTestModeActive()) - { - GetUserUnlocks(); - } - else + for (auto it = s_active_leaderboard_trackers.begin(); it != s_active_leaderboard_trackers.end();) { - ActivateLockedAchievements(); - DisplayAchievementSummary(); - Host::OnAchievementsRefreshed(); + const LeaderboardTrackerIndicator& indicator = *it; + const float opacity = IndicatorOpacity(indicator); + + const ImVec2 box_min = + ImVec2(position.x - indicator.size.x - padding * 2.0f, position.y - indicator.size.y - padding * 2.0f); + const ImVec2 box_max = position; + const float box_rounding = LayoutScale(1.0f); + dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)), + box_rounding); + dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding); + + const u32 text_col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)); + const ImVec2 text_pos = box_min + ImVec2(padding, padding); + const ImVec4 text_clip_rect(text_pos.x, text_pos.y, box_max.x, box_max.y); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, text_col, indicator.text.c_str(), + indicator.text.c_str() + indicator.text.length(), 0.0f, &text_clip_rect); + + if (!indicator.active && opacity <= 0.01f) + { + Log_DevPrintf("Remove tracker indicator"); + it = s_active_leaderboard_trackers.erase(it); + } + else + { + ++it; + } } - } - else - { - DisplayAchievementSummary(); - } - if (s_achievements.empty() && s_leaderboards.empty() && !s_has_rich_presence) - { - ClearGameInfo(); + position.y -= image_size.y + padding; } } -void Achievements::GetLbInfoCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) +void Achievements::DrawPauseMenuOverlays() { - if (!System::IsValid()) - return; + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; - RAPIResponse - response(status_code, data); - if (!response) + if (!HasActiveGame()) return; - std::unique_lock lock(s_achievements_mutex); - if (response.id != s_last_queried_lboard) - { - // User has already requested another leaderboard, drop this data - return; - } + const auto lock = GetLock(); - const Leaderboard* leaderboard = GetLeaderboardByID(response.id); - if (!leaderboard) - { - Log_ErrorPrintf("Attempting to list unknown leaderboard %u", response.id); + if (s_active_challenge_indicators.empty() && !s_active_progress_indicator.has_value()) return; - } - - s_lboard_entries = std::vector(); - for (u32 i = 0; i < response.num_entries; i++) - { - const rc_api_lboard_info_entry_t& entry = response.entries[i]; - if (!entry.username) - continue; - char score[128]; - rc_runtime_format_lboard_value(score, sizeof(score), entry.score, leaderboard->format); + const ImGuiIO& io = ImGui::GetIO(); + ImFont* font = g_medium_font; - LeaderboardEntry lbe; - lbe.user = entry.username; - lbe.rank = entry.rank; - lbe.submitted = entry.submitted; - lbe.formatted_score = score; - lbe.is_self = lbe.user == s_username; + const ImVec2 image_size(LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)); + const float start_y = LayoutScale(10.0f + 4.0f + 4.0f) + g_large_font->FontSize + (g_medium_font->FontSize * 2.0f); + const float margin = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float padding = LayoutScale(10.0f); - s_lboard_entries->push_back(std::move(lbe)); - } -} + const float max_text_width = ImGuiFullscreen::LayoutScale(300.0f); + const float row_width = max_text_width + padding + padding + image_size.x + spacing; + const float title_height = padding + font->FontSize + padding; -void Achievements::GetPatches(u32 game_id) -{ - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_id = game_id; - request.Send(GetPatchesCallback); -} + if (!s_active_challenge_indicators.empty()) + { + const ImVec2 box_min(io.DisplaySize.x - row_width - margin, start_y + margin); + const ImVec2 box_max(box_min.x + row_width, + box_min.y + title_height + + (static_cast(s_active_challenge_indicators.size()) * (image_size.y + padding))); -std::string Achievements::GetGameHash(CDImage* image) -{ - std::string executable_name; - std::vector executable_data; - if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) - return {}; + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f)); + dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255), + TRANSLATE("Achievements", "Active Challenge Achievements")); - BIOS::PSEXEHeader header; - if (executable_data.size() >= sizeof(header)) - std::memcpy(&header, executable_data.data(), sizeof(header)); - if (!BIOS::IsValidPSExeHeader(header, static_cast(executable_data.size()))) - { - Log_ErrorPrintf("PS-EXE header is invalid in '%s' (%zu bytes)", executable_name.c_str(), executable_data.size()); - return {}; - } + const float y_advance = image_size.y + spacing; + const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f; + const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f); + ImVec2 position(box_min.x + padding, box_min.y + title_height); - // See rcheevos hash.c - rc_hash_psx(). - const u32 MAX_HASH_SIZE = 64 * 1024 * 1024; - const u32 hash_size = std::min(sizeof(header) + header.file_size, MAX_HASH_SIZE); - Assert(hash_size <= executable_data.size()); + for (const AchievementChallengeIndicator& indicator : s_active_challenge_indicators) + { + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); + if (!badge) + continue; - MD5Digest digest; - digest.Update(executable_name.c_str(), static_cast(executable_name.size())); - if (hash_size > 0) - digest.Update(executable_data.data(), hash_size); + dl->AddImage(badge, position, position + image_size); - u8 hash[16]; - digest.Final(hash); + const char* achievement_title = indicator.achievement->title; + const char* achievement_title_end = achievement_title + std::strlen(indicator.achievement->title); + const char* remaining_text = nullptr; + const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title, + achievement_title_end, &remaining_text)); + const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset); + const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width, + text_position.y + image_size.y); + const u32 text_color = IM_COL32(255, 255, 255, 255); - std::string hash_str(StringUtil::StdStringFromFormat( - "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", hash[0], hash[1], hash[2], hash[3], hash[4], - hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15])); + if (remaining_text < achievement_title_end) + { + dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f, + &text_bbox); + dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...", + nullptr, 0.0f, &text_bbox); + } + else + { + dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, achievement_title_end, 0.0f, + &text_bbox); + } - Log_InfoPrintf("Hash for '%s' (%zu bytes, %u bytes hashed): %s", executable_name.c_str(), executable_data.size(), - hash_size, hash_str.c_str()); - return hash_str; + position.y += y_advance; + } + } } -void Achievements::GetGameIdCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) +bool Achievements::PrepareAchievementsWindow() { - if (!System::IsValid()) - return; + auto lock = Achievements::GetLock(); - RAPIResponse - response(status_code, data); - if (!response) - return; + s_achievement_badge_paths = {}; - const u32 game_id = response.game_id; - Log_VerbosePrintf("Server returned GameID %u", game_id); - if (game_id == 0) + if (s_achievement_list) + rc_client_destroy_achievement_list(s_achievement_list); + s_achievement_list = rc_client_create_achievement_list( + s_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS /*RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE*/); + if (!s_achievement_list) { - // We don't want to block saving/loading states when there's no achievements. - DisableChallengeMode(); - return; + Log_ErrorPrint("rc_client_create_achievement_list() returned null"); + return false; } - GetPatches(game_id); + return true; } -void Achievements::GameChanged(const std::string& path, CDImage* image) +void Achievements::DrawAchievementsWindow() { - if (!IsActive() || s_game_path == path) + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; + + if (!s_achievement_list) return; - std::unique_ptr temp_image; - if (!path.empty() && (!image || (g_settings.achievements_use_first_disc_from_playlist && image->HasSubImages() && - image->GetCurrentSubImage() != 0))) - { - temp_image = CDImage::Open(path.c_str(), g_settings.cdrom_load_image_patches, nullptr); - image = temp_image.get(); - if (!temp_image) - { - Log_ErrorPrintf("Failed to open temporary CD image '%s'", path.c_str()); - s_http_downloader->WaitForAllRequests(); - std::unique_lock lock(s_achievements_mutex); - DisableChallengeMode(); - ClearGameInfo(); - return; - } - } + auto lock = Achievements::GetLock(); - std::string game_hash; - if (image) - { - game_hash = GetGameHash(image); - if (s_game_hash == game_hash) - { - // only the path has changed - different format/save state/etc. - Log_InfoPrintf("Detected path change from '%s' to '%s'", s_game_path.c_str(), path.c_str()); - s_game_path = path; - return; - } - } + // ensure image downloads still happen while we're paused + Achievements::IdleUpdate(); - if (!IsUsingRAIntegration() && s_http_downloader->HasAnyRequests()) - { - if (image && System::IsValid()) - Host::DisplayLoadingScreen("Downloading achievements data..."); + static constexpr float alpha = 0.8f; + static constexpr float heading_alpha = 0.95f; + static constexpr float heading_height_unscaled = 110.0f; - s_http_downloader->WaitForAllRequests(); - } + const ImVec4 background(0.13f, 0.13f, 0.13f, alpha); + const ImVec4 heading_background(0.13f, 0.13f, 0.13f, heading_alpha); + const ImVec2 display_size(ImGui::GetIO().DisplaySize); + const float heading_height = ImGuiFullscreen::LayoutScale(heading_height_unscaled); - if (image && image->HasSubImages() && image->GetCurrentSubImage() != 0) + if (ImGuiFullscreen::BeginFullscreenWindow( + ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "achievements_heading", heading_background, 0.0f, + 0.0f, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) { - std::unique_ptr image_copy( - CDImage::Open(image->GetFileName().c_str(), g_settings.cdrom_load_image_patches, nullptr)); - if (!image_copy) + ImRect bb; + bool visible, hovered; + ImGuiFullscreen::MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered, + &bb.Min, &bb.Max, 0, heading_alpha); + if (visible) { - Log_ErrorPrintf("Failed to reopen image '%s'", image->GetFileName().c_str()); - return; - } + const float padding = ImGuiFullscreen::LayoutScale(10.0f); + const float spacing = ImGuiFullscreen::LayoutScale(10.0f); + const float image_height = ImGuiFullscreen::LayoutScale(85.0f); - // this will go to subimage zero automatically - Assert(image_copy->GetCurrentSubImage() == 0); - GameChanged(path, image_copy.get()); - return; - } + const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); + const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); - std::unique_lock lock(s_achievements_mutex); - if (!IsUsingRAIntegration()) - s_http_downloader->WaitForAllRequests(); + if (!s_game_icon.empty()) + { + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str()); + if (badge) + { + ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + IM_COL32(255, 255, 255, 255)); + } + } - ClearGameInfo(); - ClearGameHash(); - s_game_path = path; - s_game_hash = std::move(game_hash); + float left = bb.Min.x + padding + image_height + spacing; + float right = bb.Max.x - padding; + float top = bb.Min.y + padding; + ImDrawList* dl = ImGui::GetWindowDrawList(); + SmallString text; + ImVec2 text_size; + + if (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, + g_large_font) || + ImGuiFullscreen::WantsToCloseMenu()) + { + FullscreenUI::ReturnToMainWindow(); + } + + const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); + text = s_game_title; + + if (s_hardcore_mode) + text.AppendString(TRANSLATE_SV("Achievements", " (Hardcore Mode)")); + + top += g_large_font->FontSize + spacing; + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); + if (s_game_summary.num_unlocked_achievements == s_game_summary.num_core_achievements) + { + text.Fmt(TRANSLATE_FS("Achievements", "You have unlocked all achievements and earned {} points!"), + s_game_summary.points_unlocked); + } + else + { + text.Fmt( + TRANSLATE_FS("Achievements", "You have unlocked {} of {} achievements, earning {} of {} possible points."), + s_game_summary.num_unlocked_achievements, s_game_summary.num_core_achievements, + s_game_summary.points_unlocked, s_game_summary.points_core); + } -#ifdef WITH_RAINTEGRATION - if (IsUsingRAIntegration()) - { - RAIntegration::GameChanged(); - return; + top += g_medium_font->FontSize + spacing; + + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + + const float progress_height = ImGuiFullscreen::LayoutScale(20.0f); + const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height)); + const float fraction = static_cast(s_game_summary.num_unlocked_achievements) / + static_cast(s_game_summary.num_core_achievements); + dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); + dl->AddRectFilled(progress_bb.Min, + ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), + ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); + + text.Fmt("{}%", static_cast(std::round(fraction * 100.0f))); + text_size = ImGui::CalcTextSize(text.GetCharArray(), text.GetCharArray() + text.GetLength()); + const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), + progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - + (text_size.y / 2.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, + ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), text.GetCharArray(), + text.GetCharArray() + text.GetLength()); + top += progress_height + spacing; + } } -#endif + ImGuiFullscreen::EndFullscreenWindow(); - if (s_game_hash.empty()) + ImGui::SetNextWindowBgAlpha(alpha); + + if (ImGuiFullscreen::BeginFullscreenWindow(ImVec2(0.0f, heading_height), + ImVec2(display_size.x, display_size.y - heading_height), "achievements", + background, 0.0f, 0.0f, 0)) { - // when we're booting the bios, this will fail - if (!s_game_path.empty()) + static bool buckets_collapsed[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS] = {}; + + ImGuiFullscreen::BeginMenuButtons(); + + for (u32 bucket_type : {RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED}) { - Host::AddKeyedOSDMessage("retroachievements_disc_read_failed", - "Failed to read executable from disc. Achievements disabled.", 10.0f); + for (u32 bucket_idx = 0; bucket_idx < s_achievement_list->num_buckets; bucket_idx++) + { + const rc_client_achievement_bucket_t& bucket = s_achievement_list->buckets[bucket_idx]; + if (bucket.bucket_type != bucket_type) + continue; + + DebugAssert(bucket.bucket_type < NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS); + + // TODO: This should be translated. + bool& bucket_collapsed = buckets_collapsed[bucket.bucket_type]; + bucket_collapsed ^= ImGuiFullscreen::MenuHeadingButton(bucket.label, bucket_collapsed ? ICON_FA_CHEVRON_DOWN : + ICON_FA_CHEVRON_UP); + if (!bucket_collapsed) + { + for (u32 i = 0; i < bucket.num_achievements; i++) + DrawAchievement(bucket.achievements[i]); + } + } } - DisableChallengeMode(); - return; + ImGuiFullscreen::EndMenuButtons(); } - - if (IsLoggedIn()) - SendGetGameId(); -} - -void Achievements::SendGetGameId() -{ - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_hash = s_game_hash.c_str(); - request.Send(GetGameIdCallback); + ImGuiFullscreen::EndFullscreenWindow(); } -void Achievements::SendPlayingCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) +void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo) { - if (!System::IsValid()) - return; - - RAPIResponse - response(status_code, data); - if (!response) - return; + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; - Log_InfoPrintf("Playing game updated to %u (%s)", s_game_id, s_game_title.c_str()); -} + static constexpr float alpha = 0.8f; + static constexpr float progress_height_unscaled = 20.0f; + static constexpr float progress_spacing_unscaled = 5.0f; -void Achievements::SendPlaying() -{ - if (!HasActiveGame()) - return; + const float spacing = ImGuiFullscreen::LayoutScale(4.0f); - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_id = s_game_id; - request.Send(SendPlayingCallback); -} + const bool is_unlocked = (cheevo->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + const std::string_view measured_progress(cheevo->measured_progress); + const bool is_measured = !is_unlocked && !measured_progress.empty(); + const float unlock_size = is_unlocked ? (spacing + ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE) : 0.0f; -void Achievements::UpdateRichPresence() -{ - if (!s_has_rich_presence) + ImRect bb; + bool visible, hovered; + ImGuiFullscreen::MenuButtonFrame(TinyString::FromFmt("chv_{}", cheevo->id), true, + !is_measured ? ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + unlock_size : + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + + progress_height_unscaled + progress_spacing_unscaled, + &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); + if (!visible) return; - char buffer[512]; - const int res = - rc_runtime_get_richpresence(&s_rcheevos_runtime, buffer, sizeof(buffer), PeekMemory, nullptr, nullptr); - if (res <= 0) + std::string* badge_path; + if (const auto badge_it = std::find_if(s_achievement_badge_paths.begin(), s_achievement_badge_paths.end(), + [cheevo](const auto& it) { return (it.first == cheevo); }); + badge_it != s_achievement_badge_paths.end()) { - const bool had_rich_presence = !s_rich_presence_string.empty(); - s_rich_presence_string.clear(); - if (had_rich_presence) - Host::OnAchievementsRefreshed(); + badge_path = &badge_it->second; + } + else + { + std::string new_badge_path = Achievements::GetAchievementBadgePath(cheevo, cheevo->state); + badge_path = &s_achievement_badge_paths.emplace_back(cheevo, std::move(new_badge_path)).second; + } - return; + const ImVec2 image_size( + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT)); + if (!badge_path->empty()) + { + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(*badge_path); + if (badge) + { + ImGui::GetWindowDrawList()->AddImage(badge, bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + IM_COL32(255, 255, 255, 255)); + } } - std::unique_lock lock(s_achievements_mutex); - if (s_rich_presence_string == buffer) - return; + SmallString text; - s_rich_presence_string.assign(buffer); - Host::OnAchievementsRefreshed(); -} + const float midpoint = bb.Min.y + g_large_font->FontSize + spacing; + text.Fmt((cheevo->points != 1) ? TRANSLATE_FS("Achievements", "{} points") : TRANSLATE_FS("Achievements", "{} point"), + cheevo->points); + const ImVec2 points_template_size( + g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, TRANSLATE("Achievements", "XXX points"))); + const ImVec2 points_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text.GetCharArray(), + text.GetCharArray() + text.GetLength())); + const float points_template_start = bb.Max.x - points_template_size.x; + const float points_start = points_template_start + ((points_template_size.x - points_size.x) * 0.5f); + const char* lock_text = is_unlocked ? ICON_FA_LOCK_OPEN : ICON_FA_LOCK; + const ImVec2 lock_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, FLT_MAX, 0.0f, lock_text)); -void Achievements::SendPingCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) -{ - if (!System::IsValid()) - return; + const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(points_start, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), ImVec2(points_start, midpoint + g_medium_font->FontSize)); + const ImRect points_bb(ImVec2(points_start, midpoint), bb.Max); + const ImRect lock_bb(ImVec2(points_template_start + ((points_template_size.x - lock_size.x) * 0.5f), bb.Min.y), + ImVec2(bb.Max.x, midpoint)); - RAPIResponse response(status_code, - data); -} + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(lock_bb.Min, lock_bb.Max, lock_text, nullptr, &lock_size, ImVec2(0.0f, 0.0f), &lock_bb); + ImGui::PopFont(); -void Achievements::SendPing() -{ - if (!HasActiveGame()) - return; + ImGui::PushFont(g_medium_font); + if (cheevo->description && std::strlen(cheevo->description) > 0) + { + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, cheevo->description, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + } + ImGui::RenderTextClipped(points_bb.Min, points_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + &points_size, ImVec2(0.0f, 0.0f), &points_bb); - s_last_ping_time.Reset(); + if (is_unlocked) + { + TinyString date; + FullscreenUI::TimeToPrintableString(&date, cheevo->unlock_time); + text.Fmt(TRANSLATE_FS("Achievements", "Unlocked: {}"), date); - RAPIRequest request; - request.api_token = s_api_token.c_str(); - request.username = s_username.c_str(); - request.game_id = s_game_id; - request.rich_presence = s_rich_presence_string.c_str(); - request.Send(SendPingCallback); -} + const ImRect unlock_bb(summary_bb.Min.x, summary_bb.Max.y + spacing, summary_bb.Max.x, bb.Max.y); + ImGui::RenderTextClipped(unlock_bb.Min, unlock_bb.Max, text, text.GetCharArray() + text.GetLength(), nullptr, + ImVec2(0.0f, 0.0f), &unlock_bb); + } + else if (is_measured) + { + ImDrawList* dl = ImGui::GetWindowDrawList(); + const float progress_height = LayoutScale(progress_height_unscaled); + const float progress_spacing = LayoutScale(progress_spacing_unscaled); + const float top = midpoint + g_medium_font->FontSize + progress_spacing; + const ImRect progress_bb(ImVec2(text_start_x, top), ImVec2(bb.Max.x, top + progress_height)); + const float fraction = cheevo->measured_percent * 0.01f; + dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); + dl->AddRectFilled(progress_bb.Min, ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), + ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); -const std::string& Achievements::GetGameTitle() -{ - return s_game_title; -} + const ImVec2 text_size = + ImGui::CalcTextSize(measured_progress.data(), measured_progress.data() + measured_progress.size()); + const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), + progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, + ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), measured_progress.data(), + measured_progress.data() + measured_progress.size()); + } -const std::string& Achievements::GetGameIcon() -{ - return s_game_icon; + ImGui::PopFont(); } -bool Achievements::EnumerateAchievements(std::function callback) +bool Achievements::PrepareLeaderboardsWindow() { - for (const Achievement& cheevo : s_achievements) + auto lock = Achievements::GetLock(); + rc_client_t* const client = s_client; + + s_achievement_badge_paths = {}; + CloseLeaderboard(); + if (s_leaderboard_list) + rc_client_destroy_leaderboard_list(s_leaderboard_list); + s_leaderboard_list = rc_client_create_leaderboard_list(client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + if (!s_leaderboard_list) { - if (!callback(cheevo)) - return false; + Log_ErrorPrint("rc_client_create_leaderboard_list() returned null"); + return false; } return true; } -u32 Achievements::GetUnlockedAchiementCount() +void Achievements::DrawLeaderboardsWindow() { - u32 count = 0; - for (const Achievement& cheevo : s_achievements) - { - if (!cheevo.locked) - count++; - } - - return count; -} + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; -u32 Achievements::GetAchievementCount() -{ - return static_cast(s_achievements.size()); -} + static constexpr float alpha = 0.8f; + static constexpr float heading_alpha = 0.95f; + static constexpr float heading_height_unscaled = 110.0f; + static constexpr float tab_height_unscaled = 50.0f; -u32 Achievements::GetMaximumPointsForGame() -{ - u32 points = 0; - for (const Achievement& cheevo : s_achievements) - points += cheevo.points; + auto lock = Achievements::GetLock(); - return points; -} + // ensure image downloads still happen while we're paused + Achievements::IdleUpdate(); -u32 Achievements::GetCurrentPointsForGame() -{ - u32 points = 0; - for (const Achievement& cheevo : s_achievements) - { - if (!cheevo.locked) - points += cheevo.points; - } + const bool is_leaderboard_open = (s_open_leaderboard != nullptr); + bool close_leaderboard_on_exit = false; - return points; -} + ImRect bb; -bool Achievements::EnumerateLeaderboards(std::function callback) -{ - for (const Leaderboard& lboard : s_leaderboards) + const ImVec4 background(0.13f, 0.13f, 0.13f, alpha); + const ImVec4 heading_background(0.13f, 0.13f, 0.13f, heading_alpha); + const ImVec2 display_size(ImGui::GetIO().DisplaySize); + const float padding = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float spacing_small = spacing / 2.0f; + float heading_height = LayoutScale(heading_height_unscaled); + if (is_leaderboard_open) { - if (!callback(lboard)) - return false; + // tabs + heading_height += spacing_small + LayoutScale(tab_height_unscaled) + spacing; + + // Add space for a legend - spacing + 1 line of text + spacing + line + heading_height += LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing; } - return true; -} + const float rank_column_width = + g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, "99999").x; + const float name_column_width = + g_large_font + ->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, "WWWWWWWWWWWWWWWWWWWWWW") + .x; + const float time_column_width = + g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, "WWWWWWWWWWW").x; + const float column_spacing = spacing * 2.0f; -std::optional Achievements::TryEnumerateLeaderboardEntries(u32 id, - std::function callback) -{ - if (id == s_last_queried_lboard) + if (ImGuiFullscreen::BeginFullscreenWindow( + ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "leaderboards_heading", heading_background, 0.0f, + 0.0f, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) { - if (s_lboard_entries) + bool visible, hovered; + bool pressed = ImGuiFullscreen::MenuButtonFrame("leaderboards_heading", false, heading_height_unscaled, &visible, + &hovered, &bb.Min, &bb.Max, 0, alpha); + UNREFERENCED_VARIABLE(pressed); + + if (visible) { - for (const LeaderboardEntry& entry : *s_lboard_entries) + const float image_height = LayoutScale(85.0f); + + const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); + const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); + + if (!s_game_icon.empty()) { - if (!callback(entry)) - return false; + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str()); + if (badge) + { + ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + IM_COL32(255, 255, 255, 255)); + } } - return true; - } - } - else - { - s_last_queried_lboard = id; - s_lboard_entries.reset(); - // TODO: Add paging? For now, stick to defaults - RAPIRequest request; - request.username = s_username.c_str(); - request.leaderboard_id = id; - request.first_entry = 0; + float left = bb.Min.x + padding + image_height + spacing; + float right = bb.Max.x - padding; + float top = bb.Min.y + padding; + SmallString text; - // Just over what a single page can store, should be a reasonable amount for now - request.count = 15; - - request.Send(GetLbInfoCallback); - } + if (!is_leaderboard_open) + { + if (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, + g_large_font) || + ImGuiFullscreen::WantsToCloseMenu()) + { + FullscreenUI::ReturnToMainWindow(); + } + } + else + { + if (ImGuiFullscreen::FloatingButton(ICON_FA_CARET_SQUARE_LEFT, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, + g_large_font) || + ImGuiFullscreen::WantsToCloseMenu()) + { + close_leaderboard_on_exit = true; + } + } - return std::nullopt; -} + const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); + text.Assign(Achievements::GetGameTitle()); -const Achievements::Leaderboard* Achievements::GetLeaderboardByID(u32 id) -{ - for (const Leaderboard& lb : s_leaderboards) - { - if (lb.id == id) - return &lb; - } + top += g_large_font->FontSize + spacing; - return nullptr; -} + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); -u32 Achievements::GetLeaderboardCount() -{ - return static_cast(s_leaderboards.size()); -} + if (is_leaderboard_open) + { + const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); + text.Assign(s_open_leaderboard->title); -bool Achievements::IsLeaderboardTimeType(const Leaderboard& leaderboard) -{ - return leaderboard.format != RC_FORMAT_SCORE && leaderboard.format != RC_FORMAT_VALUE; -} + top += g_large_font->FontSize + spacing_small; -bool Achievements::IsMastered() -{ - for (const Achievement& cheevo : s_achievements) - { - if (cheevo.locked) - return false; - } + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(subtitle_bb.Min, subtitle_bb.Max, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &subtitle_bb); + ImGui::PopFont(); - return true; -} + text.Assign(s_open_leaderboard->description); + } + else + { + u32 count = 0; + for (u32 i = 0; i < s_leaderboard_list->num_buckets; i++) + count += s_leaderboard_list->buckets[i].num_leaderboards; + text.Fmt(TRANSLATE_FS("Achievements", "This game has {} leaderboards."), count); + } -void Achievements::ActivateLockedAchievements() -{ - for (Achievement& cheevo : s_achievements) - { - if (cheevo.locked) - ActivateAchievement(&cheevo); - } -} + const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); + top += g_medium_font->FontSize + spacing_small; -bool Achievements::ActivateAchievement(Achievement* achievement) -{ - if (achievement->active) - return true; + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb); - const int err = - rc_runtime_activate_achievement(&s_rcheevos_runtime, achievement->id, achievement->memaddr.c_str(), nullptr, 0); - if (err != RC_OK) - { - Log_ErrorPrintf("Achievement %u memaddr parse error: %s", achievement->id, rc_error_str(err)); - return false; - } + if (!is_leaderboard_open && !Achievements::IsHardcoreModeActive()) + { + const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); + top += g_medium_font->FontSize + spacing_small; + + ImGui::RenderTextClipped( + hardcore_warning_bb.Min, hardcore_warning_bb.Max, + TRANSLATE("Achievements", + "Submitting scores is disabled because hardcore mode is off. Leaderboards are read-only."), + nullptr, nullptr, ImVec2(0.0f, 0.0f), &hardcore_warning_bb); + } - achievement->active = true; + ImGui::PopFont(); - Log_DevPrintf("Activated achievement %s (%u)", achievement->title.c_str(), achievement->id); - return true; -} + if (is_leaderboard_open) + { + const float tab_width = (ImGui::GetWindowWidth() / ImGuiFullscreen::g_layout_scale) * 0.5f; + ImGui::SetCursorPos(ImVec2(0.0f, top + spacing_small)); + + if (ImGui::IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiNavReadMode_Pressed) || + ImGui::IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiNavReadMode_Pressed)) + { + s_is_showing_all_leaderboard_entries = !s_is_showing_all_leaderboard_entries; + } + + for (const bool show_all : {false, true}) + { + const char* title = + show_all ? TRANSLATE("Achievements", "Show Best") : TRANSLATE("Achievements", "Show Nearby"); + if (ImGuiFullscreen::NavTab(title, s_is_showing_all_leaderboard_entries == show_all, true, tab_width, + tab_height_unscaled, heading_background)) + { + s_is_showing_all_leaderboard_entries = show_all; + } + } + + const ImVec2 bg_pos = + ImVec2(0.0f, ImGui::GetCurrentWindow()->DC.CursorPos.y + LayoutScale(tab_height_unscaled)); + const ImVec2 bg_size = + ImVec2(ImGui::GetWindowWidth(), + spacing + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing); + ImGui::GetWindowDrawList()->AddRectFilled(bg_pos, bg_pos + bg_size, ImGui::GetColorU32(heading_background)); + + ImGui::SetCursorPos(ImVec2(0.0f, ImGui::GetCursorPosY() + LayoutScale(tab_height_unscaled) + spacing)); + + pressed = + ImGuiFullscreen::MenuButtonFrame("legend", false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); + UNREFERENCED_VARIABLE(pressed); + + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + float text_start_x = bb.Min.x + LayoutScale(15.0f) + padding; + + ImGui::PushFont(g_large_font); + + const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, TRANSLATE("Achievements", "Rank"), nullptr, nullptr, + ImVec2(0.0f, 0.0f), &rank_bb); + text_start_x += rank_column_width + column_spacing; + + const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, TRANSLATE("Achievements", "Name"), nullptr, nullptr, + ImVec2(0.0f, 0.0f), &user_bb); + text_start_x += name_column_width + column_spacing; + + static const char* value_headings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + TRANSLATE_NOOP("Achievements", "Time"), + TRANSLATE_NOOP("Achievements", "Score"), + TRANSLATE_NOOP("Achievements", "Value"), + }; + + const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped( + score_bb.Min, score_bb.Max, + Host::TranslateToCString( + "Achievements", + value_headings[std::min(s_open_leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)]), + nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb); + text_start_x += time_column_width + column_spacing; + + const ImRect date_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(date_bb.Min, date_bb.Max, TRANSLATE("Achievements", "Date Submitted"), nullptr, + nullptr, ImVec2(0.0f, 0.0f), &date_bb); + + ImGui::PopFont(); + + const float line_thickness = LayoutScale(1.0f); + const float line_padding = LayoutScale(5.0f); + const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); + const ImVec2 line_end(bb.Max.x, line_start.y); + ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), + line_thickness); + } + } + } + ImGuiFullscreen::EndFullscreenWindow(); -void Achievements::DeactivateAchievement(Achievement* achievement) -{ - if (!achievement->active) - return; + if (!is_leaderboard_open) + { + if (ImGuiFullscreen::BeginFullscreenWindow(ImVec2(0.0f, heading_height), + ImVec2(display_size.x, display_size.y - heading_height), "leaderboards", + background, 0.0f, 0.0f, 0)) + { + ImGuiFullscreen::BeginMenuButtons(); - rc_runtime_deactivate_achievement(&s_rcheevos_runtime, achievement->id); - achievement->active = false; + for (u32 bucket_index = 0; bucket_index < s_leaderboard_list->num_buckets; bucket_index++) + { + const rc_client_leaderboard_bucket_t& bucket = s_leaderboard_list->buckets[bucket_index]; + for (u32 i = 0; i < bucket.num_leaderboards; i++) + DrawLeaderboardListEntry(bucket.leaderboards[i]); + } - if (achievement->primed) - { - achievement->primed = false; - s_primed_achievement_count.fetch_sub(1, std::memory_order_acq_rel); + ImGuiFullscreen::EndMenuButtons(); + } + ImGuiFullscreen::EndFullscreenWindow(); } + else + { + if (ImGuiFullscreen::BeginFullscreenWindow(ImVec2(0.0f, heading_height), + ImVec2(display_size.x, display_size.y - heading_height), "leaderboard", + background, 0.0f, 0.0f, 0)) + { + ImGuiFullscreen::BeginMenuButtons(); - Log_DevPrintf("Deactivated achievement %s (%u)", achievement->title.c_str(), achievement->id); -} - -void Achievements::UnlockAchievementCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) -{ - if (!System::IsValid()) - return; + if (!s_is_showing_all_leaderboard_entries) + { + if (s_leaderboard_nearby_entries) + { + for (u32 i = 0; i < s_leaderboard_nearby_entries->num_entries; i++) + { + DrawLeaderboardEntry(s_leaderboard_nearby_entries->entries[i], + static_cast(i) == s_leaderboard_nearby_entries->user_index, rank_column_width, + name_column_width, time_column_width, column_spacing); + } + } + else + { + ImGui::PushFont(g_large_font); + + const ImVec2 pos_min(0.0f, heading_height); + const ImVec2 pos_max(display_size.x, display_size.y); + ImGui::RenderTextClipped(pos_min, pos_max, + TRANSLATE("Achievements", "Downloading leaderboard data, please wait..."), nullptr, + nullptr, ImVec2(0.5f, 0.5f)); + + ImGui::PopFont(); + } + } + else + { + for (const rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists) + { + for (u32 i = 0; i < list->num_entries; i++) + { + DrawLeaderboardEntry(list->entries[i], static_cast(i) == list->user_index, rank_column_width, + name_column_width, time_column_width, column_spacing); + } + } + + // Fetch next chunk if the loading indicator becomes visible (i.e. we scrolled enough). + bool visible, hovered; + ImGuiFullscreen::MenuButtonFrame(TRANSLATE("Achievements", "Loading..."), false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, + &bb.Min, &bb.Max); + if (visible) + { + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, TRANSLATE("Achievements", "Loading..."), nullptr, + nullptr, ImVec2(0, 0), &title_bb); + ImGui::PopFont(); + + if (!s_leaderboard_fetch_handle) + FetchNextLeaderboardEntries(); + } + } - RAPIResponse - response(status_code, data); - if (!response) - return; + ImGuiFullscreen::EndMenuButtons(); + } + ImGuiFullscreen::EndFullscreenWindow(); + } - Log_InfoPrintf("Successfully unlocked achievement %u, new score %u", response.awarded_achievement_id, - response.new_player_score); + if (close_leaderboard_on_exit) + CloseLeaderboard(); } -void Achievements::SubmitLeaderboardCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data, u32 lboard_id) +void Achievements::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self, + float rank_column_width, float name_column_width, float time_column_width, + float column_spacing) { - if (!System::IsValid()) - return; - - RAPIResponse - response(status_code, data); - if (!response) - return; + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::LayoutScale; - // Force the next leaderboard query to repopulate everything, just in case the user wants to see their new score - s_last_queried_lboard = 0; + static constexpr float alpha = 0.8f; - const Leaderboard* lb = GetLeaderboardByID(lboard_id); - if (!lb || !FullscreenUI::IsInitialized() || !g_settings.achievements_notifications) + ImRect bb; + bool visible, hovered; + bool pressed = + ImGuiFullscreen::MenuButtonFrame(entry.user, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, + &hovered, &bb.Min, &bb.Max, 0, alpha); + if (!visible) return; - char submitted_score[128]; - char best_score[128]; - rc_runtime_format_lboard_value(submitted_score, sizeof(submitted_score), response.submitted_score, lb->format); - rc_runtime_format_lboard_value(best_score, sizeof(best_score), response.best_score, lb->format); + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + float text_start_x = bb.Min.x + LayoutScale(15.0f); + SmallString text; - std::string summary = - fmt::format(TRANSLATE_FS("Achievements", "Your Score: {} (Best: {})\nLeaderboard Position: {} of {}"), - submitted_score, best_score, response.new_rank, response.num_entries); + text.Format("%u", entry.rank); - ImGuiFullscreen::AddNotification(10.0f, lb->title, std::move(summary), s_game_icon); + ImGui::PushFont(g_large_font); - // Technically not going through the resource API, but since we're passing this to something else, we can't. - if (g_settings.achievements_sound_effects) - PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, LBSUBMIT_SOUND_NAME).c_str()); -} + if (is_self) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(255, 242, 0, 255)); -void Achievements::UnlockAchievement(u32 achievement_id, bool add_notification /* = true*/) -{ - std::unique_lock lock(s_achievements_mutex); + const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + nullptr, ImVec2(0.0f, 0.0f), &rank_bb); + text_start_x += rank_column_width + column_spacing; - Achievement* achievement = GetMutableAchievementByID(achievement_id); - if (!achievement) + const float icon_size = bb.Max.y - bb.Min.y; + const ImRect icon_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + GPUTexture* icon_tex = nullptr; + if (auto it = std::find_if(s_leaderboard_user_icon_paths.begin(), s_leaderboard_user_icon_paths.end(), + [&entry](const auto& it) { return it.first == &entry; }); + it != s_leaderboard_user_icon_paths.end()) { - Log_ErrorPrintf("Attempting to unlock unknown achievement %u", achievement_id); - return; + if (!it->second.empty()) + icon_tex = ImGuiFullscreen::GetCachedTextureAsync(it->second); } - else if (!achievement->locked) + else { - Log_WarningPrintf("Achievement %u for game %u is already unlocked", achievement_id, s_game_id); - return; + std::string path = Achievements::GetLeaderboardUserBadgePath(&entry); + if (!path.empty()) + { + icon_tex = ImGuiFullscreen::GetCachedTextureAsync(path); + s_leaderboard_user_icon_paths.emplace_back(&entry, std::move(path)); + } + } + if (icon_tex) + { + ImGui::GetWindowDrawList()->AddImage(reinterpret_cast(icon_tex), icon_bb.Min, + icon_bb.Min + ImVec2(icon_size, icon_size)); } - achievement->locked = false; - DeactivateAchievement(achievement); + const ImRect user_bb(ImVec2(text_start_x + column_spacing + icon_size, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, entry.user, nullptr, nullptr, ImVec2(0.0f, 0.0f), &user_bb); + text_start_x += name_column_width + column_spacing; - Log_InfoPrintf("Achievement %s (%u) for game %u unlocked", achievement->title.c_str(), achievement_id, s_game_id); + const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, entry.display, nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb); + text_start_x += time_column_width + column_spacing; - if (FullscreenUI::IsInitialized() && g_settings.achievements_notifications) - { - std::string title; - switch (achievement->category) - { - case AchievementCategory::Local: - title = fmt::format("{} (Local)", achievement->title); - break; - case AchievementCategory::Unofficial: - title = fmt::format("{} (Unofficial)", achievement->title); - break; - case AchievementCategory::Core: - default: - title = achievement->title; - break; - } + const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + SmallString submit_time; + FullscreenUI::TimeToPrintableString(&submit_time, entry.submitted); + ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, submit_time.GetCharArray(), + submit_time.GetCharArray() + submit_time.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &time_bb); - ImGuiFullscreen::AddNotification(15.0f, std::move(title), achievement->description, - GetAchievementBadgePath(*achievement)); - } - if (g_settings.achievements_sound_effects) - PlatformMisc::PlaySoundAsync(Path::Combine(EmuFolders::Resources, UNLOCK_SOUND_NAME).c_str()); + if (is_self) + ImGui::PopStyleColor(); - if (IsMastered()) - DisplayMasteredNotification(); + ImGui::PopFont(); - if (IsTestModeActive()) + if (pressed) { - Log_WarningPrintf("Skipping sending achievement %u unlock to server because of test mode.", achievement_id); - return; + // Anything? } +} +void Achievements::DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard) +{ + using ImGuiFullscreen::g_large_font; + using ImGuiFullscreen::g_medium_font; + using ImGuiFullscreen::LayoutScale; - if (achievement->category != AchievementCategory::Core) - { - Log_WarningPrintf("Skipping sending achievement %u unlock to server because it's not from the core set.", - achievement_id); - return; - } + static constexpr float alpha = 0.8f; - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_hash = s_game_hash.c_str(); - request.achievement_id = achievement_id; - request.hardcore = static_cast(ChallengeModeActive()); - request.Send(UnlockAchievementCallback); -} + TinyString id_str; + id_str.Format("%u", lboard->id); -void Achievements::SubmitLeaderboard(u32 leaderboard_id, int value) -{ - if (IsTestModeActive()) - { - Log_WarningPrintf("Skipping sending leaderboard %u result to server because of test mode.", leaderboard_id); + ImRect bb; + bool visible, hovered; + bool pressed = ImGuiFullscreen::MenuButtonFrame(id_str, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible, + &hovered, &bb.Min, &bb.Max, 0, alpha); + if (!visible) return; - } - if (!ChallengeModeActive()) - { - Log_WarningPrintf("Skipping sending leaderboard %u result to server because Challenge mode is off.", - leaderboard_id); - return; - } + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const float text_start_x = bb.Min.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, lboard->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); - if (!LeaderboardsActive()) + if (lboard->description && lboard->description[0] != '\0') { - Log_WarningPrintf("Skipping sending leaderboard %u result to server because leaderboards are disabled.", - leaderboard_id); - return; + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, lboard->description, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); } - RAPIRequest request; - request.username = s_username.c_str(); - request.api_token = s_api_token.c_str(); - request.game_hash = s_game_hash.c_str(); - request.leaderboard_id = leaderboard_id; - request.score = value; - request.Send( - [leaderboard_id](s32 status_code, const std::string& content_type, Common::HTTPDownloader::Request::Data data) { - SubmitLeaderboardCallback(status_code, content_type, std::move(data), leaderboard_id); - }); + if (pressed) + OpenLeaderboard(lboard); } -void Achievements::AchievementPrimed(u32 achievement_id) +void Achievements::OpenLeaderboard(const rc_client_leaderboard_t* lboard) { - std::unique_lock lock(s_achievements_mutex); - Achievement* achievement = GetMutableAchievementByID(achievement_id); - if (!achievement || achievement->primed) - return; + Log_DevPrintf("Opening leaderboard '%s' (%u)", lboard->title, lboard->id); + + CloseLeaderboard(); - achievement->primed = true; - s_primed_achievement_count.fetch_add(1, std::memory_order_acq_rel); + s_open_leaderboard = lboard; + s_is_showing_all_leaderboard_entries = false; + s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries_around_user( + s_client, lboard->id, LEADERBOARD_NEARBY_ENTRIES_TO_FETCH, LeaderboardFetchNearbyCallback, nullptr); } -void Achievements::AchievementUnprimed(u32 achievement_id) +void Achievements::LeaderboardFetchNearbyCallback(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, + void* callback_userdata) { - std::unique_lock lock(s_achievements_mutex); - Achievement* achievement = GetMutableAchievementByID(achievement_id); - if (!achievement || !achievement->primed) - return; + const auto lock = GetLock(); - achievement->primed = false; - s_primed_achievement_count.fetch_sub(1, std::memory_order_acq_rel); -} + s_leaderboard_fetch_handle = nullptr; -std::pair Achievements::GetAchievementProgress(const Achievement& achievement) -{ - std::pair result; - rc_runtime_get_achievement_measured(&s_rcheevos_runtime, achievement.id, &result.first, &result.second); - return result; -} + if (result != RC_OK) + { + ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message); + CloseLeaderboard(); + return; + } -TinyString Achievements::GetAchievementProgressText(const Achievement& achievement) -{ - char buf[256]; - rc_runtime_format_achievement_measured(&s_rcheevos_runtime, achievement.id, buf, std::size(buf)); - return buf; + if (s_leaderboard_nearby_entries) + rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries); + s_leaderboard_nearby_entries = list; } -const std::string& Achievements::GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing, - bool force_unlocked_icon) +void Achievements::LeaderboardFetchAllCallback(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, + void* callback_userdata) { - const bool use_locked = (achievement.locked && !force_unlocked_icon); - std::string& badge_path = use_locked ? achievement.locked_badge_path : achievement.unlocked_badge_path; - if (!badge_path.empty() || achievement.badge_name.empty()) - return badge_path; + const auto lock = GetLock(); - // well, this comes from the internet.... :) - const std::string clean_name(Path::SanitizeFileName(achievement.badge_name)); - badge_path = - Path::Combine(s_achievement_icon_cache_directory, fmt::format("{}{}.png", clean_name, use_locked ? "_lock" : "")); - if (FileSystem::FileExists(badge_path.c_str())) - return badge_path; + s_leaderboard_fetch_handle = nullptr; - // need to download it - if (download_if_missing) + if (result != RC_OK) { - RAPIRequest request; - request.image_name = achievement.badge_name.c_str(); - request.image_type = use_locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; - request.DownloadImage(badge_path); + ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message); + CloseLeaderboard(); + return; } - return badge_path; + s_leaderboard_entry_lists.push_back(list); } -std::string Achievements::GetAchievementBadgeURL(const Achievement& achievement) +void Achievements::FetchNextLeaderboardEntries() { - RAPIRequest request; - request.image_name = achievement.badge_name.c_str(); - request.image_type = achievement.locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; - return request.GetURL(); -} + u32 start = 1; + for (rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists) + start += list->num_entries; -u32 Achievements::GetPrimedAchievementCount() -{ - // Relaxed is fine here, worst that happens is we draw the triggers one frame late. - return s_primed_achievement_count.load(std::memory_order_relaxed); + Log_DevPrintf("Fetching entries %u to %u", start, start + LEADERBOARD_ALL_FETCH_SIZE); + + if (s_leaderboard_fetch_handle) + rc_client_abort_async(s_client, s_leaderboard_fetch_handle); + s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries( + s_client, s_open_leaderboard->id, start, LEADERBOARD_ALL_FETCH_SIZE, LeaderboardFetchAllCallback, nullptr); } -void Achievements::CheevosEventHandler(const rc_runtime_event_t* runtime_event) +void Achievements::CloseLeaderboard() { - static const char* events[] = {"RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED", "RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED", - "RC_RUNTIME_EVENT_ACHIEVEMENT_RESET", "RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED", - "RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED", "RC_RUNTIME_EVENT_LBOARD_STARTED", - "RC_RUNTIME_EVENT_LBOARD_CANCELED", "RC_RUNTIME_EVENT_LBOARD_UPDATED", - "RC_RUNTIME_EVENT_LBOARD_TRIGGERED", "RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED", - "RC_RUNTIME_EVENT_LBOARD_DISABLED"}; - const char* event_text = - ((unsigned)runtime_event->type >= countof(events)) ? "unknown" : events[(unsigned)runtime_event->type]; - Log_DevPrintf("Cheevos Event %s for %u", event_text, runtime_event->id); - - switch (runtime_event->type) - { - case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - UnlockAchievement(runtime_event->id); - break; - - case RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED: - AchievementPrimed(runtime_event->id); - break; + s_leaderboard_user_icon_paths.clear(); - case RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED: - AchievementUnprimed(runtime_event->id); - break; - - case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - SubmitLeaderboard(runtime_event->id, runtime_event->value); - break; + for (auto iter = s_leaderboard_entry_lists.rbegin(); iter != s_leaderboard_entry_lists.rend(); ++iter) + rc_client_destroy_leaderboard_entry_list(*iter); + s_leaderboard_entry_lists.clear(); - default: - break; + if (s_leaderboard_nearby_entries) + { + rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries); + s_leaderboard_nearby_entries = nullptr; } -} -unsigned Achievements::PeekMemory(unsigned address, unsigned num_bytes, void* ud) -{ - switch (num_bytes) + if (s_leaderboard_fetch_handle) { - case 1: - { - u8 value = 0; - CPU::SafeReadMemoryByte(address, &value); - return value; - } - - case 2: - { - u16 value; - CPU::SafeReadMemoryHalfWord(address, &value); - return value; - } - - case 4: - { - u32 value; - CPU::SafeReadMemoryWord(address, &value); - return value; - } - - default: - return 0; + rc_client_abort_async(s_client, s_leaderboard_fetch_handle); + s_leaderboard_fetch_handle = nullptr; } + + s_open_leaderboard = nullptr; } #ifdef WITH_RAINTEGRATION #include "RA_Consoles.h" +bool Achievements::IsUsingRAIntegration() +{ + return s_using_raintegration; +} + namespace Achievements::RAIntegration { static void InitializeRAIntegration(void* main_window_handle); @@ -2046,10 +2858,6 @@ static bool s_raintegration_initialized = false; void Achievements::SwitchToRAIntegration() { s_using_raintegration = true; - s_active = true; - - // Not strictly the case, but just in case we gate anything by IsLoggedIn(). - s_logged_in = true; } void Achievements::RAIntegration::InitializeRAIntegration(void* main_window_handle) @@ -2124,12 +2932,12 @@ int Achievements::RAIntegration::RACallbackIsActive() void Achievements::RAIntegration::RACallbackCauseUnpause() { - System::PauseSystem(false); + Host::RunOnCPUThread([]() { System::PauseSystem(false); }); } void Achievements::RAIntegration::RACallbackCausePause() { - System::PauseSystem(true); + Host::RunOnCPUThread([]() { System::PauseSystem(true); }); } void Achievements::RAIntegration::RACallbackRebuildMenu() @@ -2180,4 +2988,11 @@ unsigned int Achievements::RAIntegration::RACallbackReadMemoryBlock(unsigned int return copy_size; } +#else + +bool Achievements::IsUsingRAIntegration() +{ + return false; +} + #endif diff --git a/src/core/achievements.h b/src/core/achievements.h index 91b162caf..2f71094a5 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -3,105 +3,44 @@ #pragma once -#include "settings.h" -#include "types.h" - #include "common/string.h" #include "common/types.h" -#include -#include #include #include #include +class Error; class StateWrapper; class CDImage; -namespace Achievements { - -#ifdef WITH_CHEEVOS - -enum class AchievementCategory : u8 -{ - Local = 0, - Core = 3, - Unofficial = 5 -}; - -struct Achievement -{ - u32 id; - std::string title; - std::string description; - std::string memaddr; - std::string badge_name; - - // badge paths are mutable because they're resolved when they're needed. - mutable std::string locked_badge_path; - mutable std::string unlocked_badge_path; - - u32 points; - AchievementCategory category; - bool locked; - bool active; - bool primed; -}; +struct Settings; -struct Leaderboard -{ - u32 id; - std::string title; - std::string description; - int format; -}; +namespace Achievements { -struct LeaderboardEntry +enum class LoginRequestReason { - std::string user; - std::string formatted_score; - time_t submitted; - u32 rank; - bool is_self; + UserInitiated, + TokenInvalid, }; -// RAIntegration only exists for Windows, so no point checking it on other platforms. -#ifdef WITH_RAINTEGRATION - -bool IsUsingRAIntegration(); - -#else - -static ALWAYS_INLINE bool IsUsingRAIntegration() -{ - return false; -} - -#endif - -bool IsActive(); -bool IsLoggedIn(); -bool ChallengeModeActive(); -bool LeaderboardsActive(); -bool IsTestModeActive(); -bool IsUnofficialTestModeActive(); -bool IsRichPresenceEnabled(); -bool HasActiveGame(); - -u32 GetGameID(); - /// Acquires the achievements lock. Must be held when accessing any achievement state from another thread. std::unique_lock GetLock(); -void Initialize(); +/// Initializes the RetroAchievments client. +bool Initialize(); + +/// Updates achievements settings. void UpdateSettings(const Settings& old_config); -void ResetRuntime(); + +/// Resets the internal state of all achievement tracking. Call on system reset. +void ResetClient(); /// Called when the system is being reset. If it returns false, the reset should be aborted. bool ConfirmSystemReset(); /// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted. -bool Shutdown(); +bool Shutdown(bool allow_cancel); /// Called when the system is being paused and resumed. void OnSystemPaused(bool paused); @@ -110,60 +49,86 @@ void OnSystemPaused(bool paused); void FrameUpdate(); /// Called when the system is paused, because FrameUpdate() won't be getting called. -void ProcessPendingHTTPRequests(); +void IdleUpdate(); /// Saves/loads state. bool DoState(StateWrapper& sw); -/// Returns true if the current game has any achievements or leaderboards. -/// Does not need to have the lock held. -bool SafeHasAchievementsOrLeaderboards(); - -const std::string& GetUsername(); -const std::string& GetRichPresenceString(); +/// Attempts to log in to RetroAchievements using the specified credentials. +/// If the login is successful, the token returned by the server will be saved. +bool Login(const char* username, const char* password, Error* error); -bool LoginAsync(const char* username, const char* password); -bool Login(const char* username, const char* password); +/// Logs out of RetroAchievements, clearing any credentials. void Logout(); +/// Called when the system changes game, or is booting. void GameChanged(const std::string& path, CDImage* image); /// Re-enables hardcode mode if it is enabled in the settings. -bool ResetChallengeMode(); +bool ResetHardcoreMode(); /// Forces hardcore mode off until next reset. -void DisableChallengeMode(); +void DisableHardcoreMode(); /// Prompts the user to disable hardcore mode, if they agree, returns true. -bool ConfirmChallengeModeDisable(const char* trigger); +bool ConfirmHardcoreModeDisable(const char* trigger); + +/// Returns true if hardcore mode is active, and functionality should be restricted. +bool IsHardcoreModeActive(); + +/// RAIntegration only exists for Windows, so no point checking it on other platforms. +bool IsUsingRAIntegration(); + +/// Returns true if the achievement system is active. Achievements can be active without a valid client. +bool IsActive(); + +/// Returns true if RetroAchievements game data has been loaded. +bool HasActiveGame(); + +/// Returns the RetroAchievements ID for the current game. +u32 GetGameID(); + +/// Returns true if the current game has any achievements or leaderboards. +bool HasAchievementsOrLeaderboards(); + +/// Returns true if the current game has any leaderboards. +bool HasLeaderboards(); + +/// Returns true if the game supports rich presence. +bool HasRichPresence(); -/// Returns true if features such as save states should be disabled. -bool ChallengeModeActive(); +/// Returns the current rich presence string. +/// Should be called with the lock held. +const std::string& GetRichPresenceString(); +/// Returns the RetroAchievements title for the current game. +/// Should be called with the lock held. const std::string& GetGameTitle(); -const std::string& GetGameIcon(); - -bool EnumerateAchievements(std::function callback); -u32 GetUnlockedAchiementCount(); -u32 GetAchievementCount(); -u32 GetMaximumPointsForGame(); -u32 GetCurrentPointsForGame(); - -bool EnumerateLeaderboards(std::function callback); -std::optional TryEnumerateLeaderboardEntries(u32 id, std::function callback); -const Leaderboard* GetLeaderboardByID(u32 id); -u32 GetLeaderboardCount(); -bool IsLeaderboardTimeType(const Leaderboard& leaderboard); -u32 GetPrimedAchievementCount(); - -const Achievement* GetAchievementByID(u32 id); -std::pair GetAchievementProgress(const Achievement& achievement); -TinyString GetAchievementProgressText(const Achievement& achievement); -const std::string& GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing = true, - bool force_unlocked_icon = false); -std::string GetAchievementBadgeURL(const Achievement& achievement); + +/// Clears all cached state used to render the UI. +void ClearUIState(); + +/// Draws ImGui overlays when not paused. +void DrawGameOverlays(); + +/// Draws ImGui overlays when paused. +void DrawPauseMenuOverlays(); + +/// Queries the achievement list, and if no achievements are available, returns false. +bool PrepareAchievementsWindow(); + +/// Renders the achievement list. +void DrawAchievementsWindow(); + +/// Queries the leaderboard list, and if no leaderboards are available, returns false. +bool PrepareLeaderboardsWindow(); + +/// Renders the leaderboard list. +void DrawLeaderboardsWindow(); #ifdef WITH_RAINTEGRATION +/// Prevents the internal implementation from being used. Instead, RAIntegration will be +/// called into when achievement-related events occur. void SwitchToRAIntegration(); namespace RAIntegration { @@ -173,46 +138,20 @@ std::vector> GetMenuItems(); void ActivateMenuItem(int item); } // namespace RAIntegration #endif - -#else - -// Make noops when compiling without cheevos. -static inline bool ConfirmSystemReset() -{ - return true; -} -static inline void ResetRuntime() -{ -} -static inline bool DoState(StateWrapper& sw) -{ - return true; -} -static constexpr inline bool ChallengeModeActive() -{ - return false; -} - -static inline bool ResetChallengeMode() -{ - return false; -} - -static inline void DisableChallengeMode() -{ -} - -static inline bool ConfirmChallengeModeDisable(const char* trigger) -{ - return true; -} - -#endif - } // namespace Achievements /// Functions implemented in the frontend. namespace Host { +/// Called if the big picture UI requests achievements login, or token login fails. +void OnAchievementsLoginRequested(Achievements::LoginRequestReason reason); + +/// Called when achievements login completes. +void OnAchievementsLoginSuccess(const char* display_name, u32 points, u32 sc_points, u32 unread_messages); + +/// Called whenever game details or rich presence information is updated. +/// Implementers can assume the lock is held when this is called. void OnAchievementsRefreshed(); -void OnAchievementsChallengeModeChanged(); + +/// Called whenever hardcore mode is toggled. +void OnAchievementsHardcoreModeChanged(); } // namespace Host diff --git a/src/core/core.props b/src/core/core.props index d4c832dba..437a4d334 100644 --- a/src/core/core.props +++ b/src/core/core.props @@ -4,7 +4,7 @@ - WITH_CHEEVOS=1;WITH_DISCORD_PRESENCE=1;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;%(PreprocessorDefinitions) WITH_RAINTEGRATION=1;%(PreprocessorDefinitions) WITH_RECOMPILER=1;%(PreprocessorDefinitions) WITH_MMAP_FASTMEM=1;%(PreprocessorDefinitions) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index ceaf40b1f..28420f114 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #define IMGUI_DEFINE_MATH_OPERATORS @@ -54,7 +54,7 @@ Log_SetChannel(FullscreenUI); #define TR_CONTEXT "FullscreenUI" -namespace { +namespace FullscreenUI { template class IconStackString : public StackString { @@ -64,9 +64,9 @@ public: StackString::Fmt("{} {}", icon, Host::TranslateToStringView(TR_CONTEXT, str)); } }; -} // namespace +} // namespace FullscreenUI -#define FSUI_ICONSTR(icon, str) IconStackString<128>(icon, str).GetCharArray() +#define FSUI_ICONSTR(icon, str) FullscreenUI::IconStackString<128>(icon, str).GetCharArray() #define FSUI_STR(str) Host::TranslateToString(TR_CONTEXT, str) #define FSUI_CSTR(str) Host::TranslateToCString(TR_CONTEXT, str) #define FSUI_VSTR(str) Host::TranslateToStringView(TR_CONTEXT, str) @@ -135,6 +135,7 @@ using ImGuiFullscreen::MenuImageButton; using ImGuiFullscreen::ModAlpha; using ImGuiFullscreen::MulAlpha; using ImGuiFullscreen::NavButton; +using ImGuiFullscreen::NavTab; using ImGuiFullscreen::NavTitle; using ImGuiFullscreen::OpenChoiceDialog; using ImGuiFullscreen::OpenConfirmMessageDialog; @@ -160,20 +161,15 @@ enum class MainWindowType GameList, Settings, PauseMenu, -#ifdef WITH_CHEEVOS Achievements, Leaderboards, -#endif - }; enum class PauseSubMenu { None, Exit, -#ifdef WITH_CHEEVOS Achievements, -#endif }; enum class SettingsPage @@ -211,7 +207,6 @@ struct PostProcessingStageInfo ////////////////////////////////////////////////////////////////////////// // Utility ////////////////////////////////////////////////////////////////////////// -static std::string TimeToPrintableString(time_t t); static void StartAsyncOp(std::function callback, std::string name); static void AsyncOpThreadEntryPoint(std::function callback, FullscreenUI::ProgressCallback* progress); @@ -225,7 +220,6 @@ static void ToggleTheme(); static void PauseForMenuOpen(); static void ClosePauseMenu(); static void OpenPauseSubMenu(PauseSubMenu submenu); -static void ReturnToMainWindow(); static void DrawLandingWindow(); static void DrawPauseMenu(MainWindowType type); static void ExitFullscreenAndOpenURL(const std::string_view& url); @@ -306,7 +300,6 @@ static void DrawMemoryCardSettingsPage(); static void DrawControllerSettingsPage(); static void DrawHotkeySettingsPage(); static void DrawAchievementsSettingsPage(); -static void DrawAchievementsLoginWindow(); static void DrawAdvancedSettingsPage(); static bool IsEditingGameSettings(SettingsInterface* bsi); @@ -473,29 +466,13 @@ static GPUTexture* GetCoverForCurrentGame(); static std::unordered_map s_cover_image_map; static std::vector s_game_list_sorted_entries; static GameListPage s_game_list_page = GameListPage::Grid; - -#ifdef WITH_CHEEVOS -////////////////////////////////////////////////////////////////////////// -// Achievements -////////////////////////////////////////////////////////////////////////// -static void DrawAchievementsWindow(); -static void DrawAchievement(const Achievements::Achievement& cheevo); -static void DrawPrimedAchievementsIcons(); -static void DrawPrimedAchievementsList(); -static void DrawLeaderboardsWindow(); -static void DrawLeaderboardListEntry(const Achievements::Leaderboard& lboard); -static void DrawLeaderboardEntry(const Achievements::LeaderboardEntry& lbEntry, float rank_column_width, - float name_column_width, float column_spacing); - -static std::optional s_open_leaderboard_id; -#endif } // namespace FullscreenUI ////////////////////////////////////////////////////////////////////////// // Utility ////////////////////////////////////////////////////////////////////////// -std::string FullscreenUI::TimeToPrintableString(time_t t) +void FullscreenUI::TimeToPrintableString(String* str, time_t t) { struct tm lt = {}; #ifdef _MSC_VER @@ -506,7 +483,7 @@ std::string FullscreenUI::TimeToPrintableString(time_t t) char buf[256]; std::strftime(buf, sizeof(buf), "%c", <); - return std::string(buf); + str->Assign(buf); } void FullscreenUI::StartAsyncOp(std::function callback, std::string name) @@ -634,7 +611,6 @@ void FullscreenUI::CheckForConfigChanges(const Settings& old_settings) if (!IsInitialized()) return; -#ifdef WITH_CHEEVOS // If achievements got disabled, we might have the menu open... // That means we're going to be reading achievement state. if (old_settings.achievements_enabled && !g_settings.achievements_enabled) @@ -642,7 +618,6 @@ void FullscreenUI::CheckForConfigChanges(const Settings& old_settings) if (s_current_main_window == MainWindowType::Achievements || s_current_main_window == MainWindowType::Leaderboards) ReturnToMainWindow(); } -#endif } void FullscreenUI::OnSystemStarted() @@ -742,6 +717,7 @@ void FullscreenUI::OpenPauseSubMenu(PauseSubMenu submenu) void FullscreenUI::Shutdown() { + Achievements::ClearUIState(); CancelAsyncOps(); CloseSaveStateSelector(); s_cover_image_map.clear(); @@ -770,14 +746,9 @@ void FullscreenUI::Render() ImGuiFullscreen::BeginLayout(); -#ifdef WITH_CHEEVOS // Primed achievements must come first, because we don't want the pause screen to be behind them. - if (g_settings.achievements_primed_indicators && s_current_main_window == MainWindowType::None && - Achievements::GetPrimedAchievementCount() > 0) - { - DrawPrimedAchievementsIcons(); - } -#endif + if (s_current_main_window == MainWindowType::None) + Achievements::DrawGameOverlays(); switch (s_current_main_window) { @@ -793,14 +764,12 @@ void FullscreenUI::Render() case MainWindowType::PauseMenu: DrawPauseMenu(s_current_main_window); break; -#ifdef WITH_CHEEVOS case MainWindowType::Achievements: - DrawAchievementsWindow(); + Achievements::DrawAchievementsWindow(); break; case MainWindowType::Leaderboards: - DrawLeaderboardsWindow(); + Achievements::DrawLeaderboardsWindow(); break; -#endif default: break; } @@ -4443,8 +4412,6 @@ void FullscreenUI::DrawAudioSettingsPage() EndMenuButtons(); } -#ifdef WITH_CHEEVOS - void FullscreenUI::DrawAchievementsSettingsPage() { #ifdef WITH_RAINTEGRATION @@ -4462,7 +4429,7 @@ void FullscreenUI::DrawAchievementsSettingsPage() const auto lock = Achievements::GetLock(); if (Achievements::IsActive() && !System::IsRunning()) - Achievements::ProcessPendingHTTPRequests(); + Achievements::IdleUpdate(); SettingsInterface* bsi = GetEditingSettingsInterface(); @@ -4474,12 +4441,8 @@ void FullscreenUI::DrawAchievementsSettingsPage() "Cheevos", "Enabled", false); const bool enabled = bsi->GetBoolValue("Cheevos", "Enabled", false); - const bool challenge = bsi->GetBoolValue("Cheevos", "ChallengeMode", false); + const bool hardcore = bsi->GetBoolValue("Cheevos", "ChallengeMode", false); - DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_USER_FRIENDS, "Rich Presence"), - FSUI_CSTR("When enabled, rich presence information will be collected and sent to the server where supported."), - "Cheevos", "RichPresence", true, enabled); if (DrawToggleSetting( bsi, FSUI_ICONSTR(ICON_FA_HARD_HAT, "Hardcore Mode"), FSUI_CSTR("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, " @@ -4487,188 +4450,101 @@ void FullscreenUI::DrawAchievementsSettingsPage() "Cheevos", "ChallengeMode", false, enabled)) { if (System::IsValid() && bsi->GetBoolValue("Cheevos", "ChallengeMode", false)) - ShowToast(std::string(), "Hardcore mode will be enabled on next game restart."); + ShowToast(std::string(), FSUI_STR("Hardcore mode will be enabled on next game restart.")); } - DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST_OL, "Leaderboards"), - FSUI_CSTR("Enables tracking and submission of leaderboards in supported games."), "Cheevos", - "Leaderboards", true, enabled && challenge); DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_INBOX, "Show Notifications"), + bsi, FSUI_ICONSTR(ICON_FA_INBOX, "Achievement Notifications"), FSUI_CSTR("Displays popup messages on events such as achievement unlocks and leaderboard submissions."), "Cheevos", "Notifications", true, enabled); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST_OL, "Leaderboard Notifications"), + FSUI_CSTR("Displays popup messages when starting, submitting, or failing a leaderboard challenge."), + "Cheevos", "LeaderboardNotifications", true, enabled && hardcore); DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_HEADPHONES, "Enable Sound Effects"), + bsi, FSUI_ICONSTR(ICON_FA_HEADPHONES, "Sound Effects"), FSUI_CSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos", "SoundEffects", true, enabled); DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Show Challenge Indicators"), + bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Enable In-Game Overlays"), FSUI_CSTR("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active."), - "Cheevos", "PrimedIndicators", true, enabled); + "Cheevos", "Overlays", true, enabled); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_USER_FRIENDS, "Encore Mode"), + FSUI_CSTR("When enabled, each session will behave as if no achievements have been unlocked."), + "Cheevos", "EncoreMode", false, enabled); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_STETHOSCOPE, "Spectator Mode"), + FSUI_CSTR("When enabled, DuckStation will assume all achievements are locked and not send any " + "unlock notifications to the server."), + "Cheevos", "SpectatorMode", false, enabled); DrawToggleSetting( bsi, FSUI_ICONSTR(ICON_FA_MEDAL, "Test Unofficial Achievements"), FSUI_CSTR("When enabled, DuckStation will list achievements from unofficial sets. These achievements are not " "tracked by RetroAchievements."), "Cheevos", "UnofficialTestMode", false, enabled); - DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_STETHOSCOPE, "Test Mode"), - FSUI_CSTR("When enabled, DuckStation will assume all achievements are locked and not send any unlock " - "notifications to the server."), - "Cheevos", "TestMode", false, enabled); - MenuHeading("Account"); - if (Achievements::IsLoggedIn()) + if (!IsEditingGameSettings(bsi)) { - ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); - ActiveButton( - SmallString::FromFmt(fmt::runtime(FSUI_ICONSTR(ICON_FA_USER, "Username: {}")), Achievements::GetUsername()), - false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - - TinyString ts_string; - ts_string.AppendFmtString( - "{:%Y-%m-%d %H:%M:%S}", - fmt::localtime(StringUtil::FromChars(bsi->GetStringValue("Cheevos", "LoginTimestamp", "0")).value_or(0))); - ActiveButton(SmallString::FromFmt(fmt::runtime(FSUI_ICONSTR(ICON_FA_CLOCK, "Login token generated on {}")), - ts_string.GetCharArray()), - false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - ImGui::PopStyleColor(); - - if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Logout"), FSUI_CSTR("Logs out of RetroAchievements."))) + MenuHeading("Account"); + if (bsi->ContainsValue("Cheevos", "Token")) { - Host::RunOnCPUThread([]() { Achievements::Logout(); }); - } - } - else if (Achievements::IsActive()) - { - ActiveButton(FSUI_ICONSTR(ICON_FA_USER, "Not Logged In"), false, false, - ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - - if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), FSUI_CSTR("Logs in to RetroAchievements."))) - ImGui::OpenPopup("Achievements Login"); - - DrawAchievementsLoginWindow(); - } - else - { - ActiveButton(FSUI_ICONSTR(ICON_FA_USER, "Achievements are disabled."), false, false, - ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - } - - MenuHeading("Current Game"); - if (Achievements::HasActiveGame()) - { - ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); - ActiveButton( - fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_BOOKMARK, "Game ID: {}")), Achievements::GetGameID()).c_str(), - false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - ActiveButton( - fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_BOOK, "Game Title: {}")), Achievements::GetGameTitle()).c_str(), - false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - ActiveButton(fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_TROPHY, "Achievements: {} ({} points)")), - Achievements::GetAchievementCount(), Achievements::GetMaximumPointsForGame()) - .c_str(), - false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - - const std::string& rich_presence_string = Achievements::GetRichPresenceString(); - if (!rich_presence_string.empty()) - { - ActiveButton(SmallString::FromFmt(ICON_FA_MAP "{}", rich_presence_string), false, false, - LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); + ActiveButton(SmallString::FromFmt(fmt::runtime(FSUI_ICONSTR(ICON_FA_USER, "Username: {}")), + bsi->GetStringValue("Cheevos", "Username")), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + TinyString ts_string; + ts_string.AppendFmtString( + "{:%Y-%m-%d %H:%M:%S}", + fmt::localtime(StringUtil::FromChars(bsi->GetStringValue("Cheevos", "LoginTimestamp", "0")).value_or(0))); + ActiveButton(SmallString::FromFmt(fmt::runtime(FSUI_ICONSTR(ICON_FA_CLOCK, "Login token generated on {}")), + ts_string.GetCharArray()), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ImGui::PopStyleColor(); + + if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Logout"), FSUI_CSTR("Logs out of RetroAchievements."))) + { + Host::RunOnCPUThread([]() { Achievements::Logout(); }); + } } else { - ActiveButton(FSUI_ICONSTR(ICON_FA_MAP, "Rich presence inactive or unsupported."), false, false, - LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - } - - ImGui::PopStyleColor(); - } - else - { - ActiveButton(FSUI_ICONSTR(ICON_FA_BAN, "Game not loaded or no RetroAchievements available."), false, false, - LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - } - - EndMenuButtons(); -} - -void FullscreenUI::DrawAchievementsLoginWindow() -{ - ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ActiveButton(FSUI_ICONSTR(ICON_FA_USER, "Not Logged In"), false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); - ImGui::PushFont(g_large_font); - - bool is_open = true; - if (ImGui::BeginPopupModal("Achievements Login", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) - { - - ImGui::TextWrapped("%s", FSUI_CSTR("Please enter your user name and password for retroachievements.org.")); - ImGui::NewLine(); - ImGui::TextWrapped( - "%s", - FSUI_CSTR("Your password will not be saved in DuckStation, an access token will be generated and used instead.")); - - ImGui::NewLine(); - - static char username[256] = {}; - static char password[256] = {}; - - ImGui::TextUnformatted(FSUI_CSTR("User Name: ")); - ImGui::SameLine(LayoutScale(200.0f)); - ImGui::InputText("##username", username, sizeof(username)); - - ImGui::TextUnformatted(FSUI_CSTR("Password: ")); - ImGui::SameLine(LayoutScale(200.0f)); - ImGui::InputText("##password", password, sizeof(password), ImGuiInputTextFlags_Password); - - ImGui::NewLine(); + if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), FSUI_CSTR("Logs in to RetroAchievements."))) + Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason::UserInitiated); + } - BeginMenuButtons(); + MenuHeading("Current Game"); + if (Achievements::HasActiveGame()) + { + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); + ActiveButton(SmallString::FromFmt(fmt::runtime(FSUI_ICONSTR(ICON_FA_BOOKMARK, "Game: {} ({})")), + Achievements::GetGameID(), Achievements::GetGameTitle()), + false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - const bool login_enabled = (std::strlen(username) > 0 && std::strlen(password) > 0); + const std::string& rich_presence_string = Achievements::GetRichPresenceString(); + if (!rich_presence_string.empty()) + { + ActiveButton(SmallString::FromFmt(ICON_FA_MAP "{}", rich_presence_string), false, false, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + else + { + ActiveButton(FSUI_ICONSTR(ICON_FA_MAP, "Rich presence inactive or unsupported."), false, false, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } - if (ActiveButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), false, login_enabled)) - { - Achievements::LoginAsync(username, password); - std::memset(username, 0, sizeof(username)); - std::memset(password, 0, sizeof(password)); - ImGui::CloseCurrentPopup(); + ImGui::PopStyleColor(); } - - if (ActiveButton(FSUI_ICONSTR(ICON_FA_TIMES, "Cancel"), false)) + else { - std::memset(username, 0, sizeof(username)); - std::memset(password, 0, sizeof(password)); - ImGui::CloseCurrentPopup(); + ActiveButton(FSUI_ICONSTR(ICON_FA_BAN, "Game not loaded or no RetroAchievements available."), false, false, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); } - - EndMenuButtons(); - - ImGui::EndPopup(); } - ImGui::PopFont(); - ImGui::PopStyleVar(2); -} - -#else - -void FullscreenUI::DrawAchievementsSettingsPage() -{ - BeginMenuButtons(); - ActiveButton(FSUI_ICONSTR(ICON_FA_BAN, "This build was not compiled with RetroAchivements support."), false, false, - ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); EndMenuButtons(); } -void FullscreenUI::DrawAchievementsLoginWindow() -{ -} - -#endif - void FullscreenUI::DrawAdvancedSettingsPage() { SettingsInterface* bsi = GetEditingSettingsInterface(); @@ -4806,9 +4682,9 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) title_pos.y + g_large_font->FontSize + LayoutScale(4.0f)); float rp_height = 0.0f; -#ifdef WITH_CHEEVOS - if (Achievements::IsActive()) + if (Achievements::HasActiveGame()) { + const auto lock = Achievements::GetLock(); const std::string& rp = Achievements::GetRichPresenceString(); if (!rp.empty()) { @@ -4829,7 +4705,6 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) wrap_width); } } -#endif DrawShadowedText(dl, g_large_font, title_pos, IM_COL32(255, 255, 255, 255), title.c_str()); DrawShadowedText(dl, g_medium_font, subtitle_pos, IM_COL32(255, 255, 255, 255), buffer); @@ -4887,9 +4762,7 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) static constexpr u32 submenu_item_count[] = { 12, // None 4, // Exit -#ifdef WITH_CHEEVOS 3, // Achievements -#endif }; const bool just_focused = ResetFocusHere(); @@ -4926,7 +4799,7 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) } if (ActiveButton(FSUI_ICONSTR(ICON_FA_FROWN_OPEN, "Cheat List"), false, - !System::GetGameSerial().empty() && !Achievements::ChallengeModeActive())) + !System::GetGameSerial().empty() && !Achievements::IsHardcoreModeActive())) { s_current_main_window = MainWindowType::None; DoCheatsMenu(); @@ -4943,21 +4816,15 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) SwitchToGameSettings(); } -#ifdef WITH_CHEEVOS if (ActiveButton(FSUI_ICONSTR(ICON_FA_TROPHY, "Achievements"), false, - Achievements::HasActiveGame() && Achievements::SafeHasAchievementsOrLeaderboards())) + Achievements::HasActiveGame() && Achievements::HasAchievementsOrLeaderboards())) { - const auto lock = Achievements::GetLock(); - // skip second menu and go straight to cheevos if there's no lbs - if (Achievements::GetLeaderboardCount() == 0) + if (!Achievements::HasLeaderboards()) OpenAchievementsWindow(); else OpenPauseSubMenu(PauseSubMenu::Achievements); } -#else - ActiveButton(FSUI_ICONSTR(ICON_FA_TROPHY, "Achievements"), false, false); -#endif if (ActiveButton(FSUI_ICONSTR(ICON_FA_CAMERA, "Save Screenshot"), false)) { @@ -5010,7 +4877,6 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) } break; -#ifdef WITH_CHEEVOS case PauseSubMenu::Achievements: { if (ActiveButton(FSUI_ICONSTR(ICON_FA_BACKWARD, "Back To Pause Menu"), false)) @@ -5023,7 +4889,6 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) OpenLeaderboardsWindow(); } break; -#endif } EndMenuButtons(); @@ -5031,10 +4896,7 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) EndFullscreenWindow(); } -#ifdef WITH_CHEEVOS - if (Achievements::GetPrimedAchievementCount() > 0) - DrawPrimedAchievementsList(); -#endif + Achievements::DrawPauseMenuOverlays(); } void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, @@ -5521,9 +5383,11 @@ void FullscreenUI::DrawResumeStateSelector() ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) { const SaveStateListEntry& entry = s_save_state_selector_slots.front(); + SmallString time; + TimeToPrintableString(&time, entry.timestamp); ImGui::TextWrapped( FSUI_CSTR("A resume save state created at %s was found.\n\nDo you want to load this save and continue?"), - TimeToPrintableString(entry.timestamp).c_str()); + time.GetCharArray()); const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); const float image_height = LayoutScale(250.0f); @@ -6593,12 +6457,13 @@ bool FullscreenUI::DrawConfirmWindow(const char* message, bool* result) return !is_open; } -#ifdef WITH_CHEEVOS - bool FullscreenUI::OpenAchievementsWindow() { - if (!System::IsValid() || !Achievements::HasActiveGame() || Achievements::GetAchievementCount() == 0 || !Initialize()) + if (!System::IsValid() || !Achievements::HasActiveGame() || !Initialize() || + !Achievements::PrepareAchievementsWindow()) + { return false; + } if (s_current_main_window != MainWindowType::PauseMenu) PauseForMenuOpen(); @@ -6608,736 +6473,29 @@ bool FullscreenUI::OpenAchievementsWindow() return true; } -void FullscreenUI::DrawAchievement(const Achievements::Achievement& cheevo) +bool FullscreenUI::IsAchievementsWindowOpen() { - static constexpr float alpha = 0.8f; - static constexpr float progress_height_unscaled = 20.0f; - static constexpr float progress_spacing_unscaled = 5.0f; - - std::string id_str(fmt::format("chv_{}", cheevo.id)); - - const auto progress = Achievements::GetAchievementProgress(cheevo); - const bool is_measured = progress.second != 0; - - ImRect bb; - bool visible, hovered; - MenuButtonFrame(id_str.c_str(), true, - !is_measured ? LAYOUT_MENU_BUTTON_HEIGHT : - LAYOUT_MENU_BUTTON_HEIGHT + progress_height_unscaled + progress_spacing_unscaled, - &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); - if (!visible) - return; - - const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); - const std::string& badge_path = Achievements::GetAchievementBadgePath(cheevo); - if (!badge_path.empty()) - { - GPUTexture* badge = GetCachedTextureAsync(badge_path.c_str()); - if (badge) - { - ImGui::GetWindowDrawList()->AddImage(badge, bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), - IM_COL32(255, 255, 255, 255)); - } - } - - const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); - const auto points_text = - TinyString::FromFmt((cheevo.points != 1) ? FSUI_FSTR("{} points") : FSUI_FSTR("{} point"), cheevo.points); - const ImVec2 points_template_size( - g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, FSUI_CSTR("XXX points"))); - const ImVec2 points_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, - points_text.GetCharArray(), - points_text.GetCharArray() + points_text.GetLength())); - const float points_template_start = bb.Max.x - points_template_size.x; - const float points_start = points_template_start + ((points_template_size.x - points_size.x) * 0.5f); - const char* lock_text = cheevo.locked ? ICON_FA_LOCK : ICON_FA_LOCK_OPEN; - const ImVec2 lock_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, FLT_MAX, 0.0f, lock_text)); - - const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); - const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(points_start, midpoint)); - const ImRect summary_bb(ImVec2(text_start_x, midpoint), ImVec2(points_start, bb.Max.y)); - const ImRect points_bb(ImVec2(points_start, midpoint), bb.Max); - const ImRect lock_bb(ImVec2(points_template_start + ((points_template_size.x - lock_size.x) * 0.5f), bb.Min.y), - ImVec2(bb.Max.x, midpoint)); - - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo.title.c_str(), cheevo.title.c_str() + cheevo.title.size(), - nullptr, ImVec2(0.0f, 0.0f), &title_bb); - ImGui::RenderTextClipped(lock_bb.Min, lock_bb.Max, lock_text, nullptr, &lock_size, ImVec2(0.0f, 0.0f), &lock_bb); - ImGui::PopFont(); - - ImGui::PushFont(g_medium_font); - if (!cheevo.description.empty()) - { - ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, cheevo.description.c_str(), - cheevo.description.c_str() + cheevo.description.size(), nullptr, ImVec2(0.0f, 0.0f), - &summary_bb); - } - ImGui::RenderTextClipped(points_bb.Min, points_bb.Max, points_text.GetCharArray(), - points_text.GetCharArray() + points_text.GetLength(), &points_size, ImVec2(0.0f, 0.0f), - &points_bb); - ImGui::PopFont(); - - if (is_measured) - { - ImDrawList* dl = ImGui::GetWindowDrawList(); - const float progress_height = LayoutScale(progress_height_unscaled); - const float progress_spacing = LayoutScale(progress_spacing_unscaled); - const float top = midpoint + g_medium_font->FontSize + progress_spacing; - const ImRect progress_bb(ImVec2(text_start_x, top), ImVec2(bb.Max.x, top + progress_height)); - const float fraction = static_cast(progress.first) / static_cast(progress.second); - dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); - dl->AddRectFilled(progress_bb.Min, ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), - ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); - - const auto text = Achievements::GetAchievementProgressText(cheevo); - const ImVec2 text_size = ImGui::CalcTextSize(text.GetCharArray(), text.GetCharArray() + text.GetLength()); - const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), - progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)); - dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, - ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), text.GetCharArray(), - text.GetCharArray() + text.GetLength()); - } -} - -void FullscreenUI::DrawAchievementsWindow() -{ - // ensure image downloads still happen while we're paused - Achievements::ProcessPendingHTTPRequests(); - - static constexpr float alpha = 0.8f; - static constexpr float heading_height_unscaled = 110.0f; - - ImGui::SetNextWindowBgAlpha(alpha); - - const ImVec4 background(0.13f, 0.13f, 0.13f, alpha); - const ImVec2 display_size(ImGui::GetIO().DisplaySize); - const float heading_height = LayoutScale(heading_height_unscaled); - - if (BeginFullscreenWindow( - ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "achievements_heading", background, 0.0f, 0.0f, - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) - { - auto lock = Achievements::GetLock(); - - ImRect bb; - bool visible, hovered; - /*bool pressed = */ MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered, - &bb.Min, &bb.Max, 0, alpha); - - if (visible) - { - const float padding = LayoutScale(10.0f); - const float spacing = LayoutScale(10.0f); - const float image_height = LayoutScale(85.0f); - - const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); - const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); - - const std::string& icon_path = Achievements::GetGameIcon(); - if (!icon_path.empty()) - { - GPUTexture* badge = GetCachedTexture(icon_path.c_str()); - if (badge) - { - ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), - IM_COL32(255, 255, 255, 255)); - } - } - - float left = bb.Min.x + padding + image_height + spacing; - float right = bb.Max.x - padding; - float top = bb.Min.y + padding; - ImDrawList* dl = ImGui::GetWindowDrawList(); - std::string text; - ImVec2 text_size; - - const u32 unlocked_count = Achievements::GetUnlockedAchiementCount(); - const u32 achievement_count = Achievements::GetAchievementCount(); - const u32 current_points = Achievements::GetCurrentPointsForGame(); - const u32 total_points = Achievements::GetMaximumPointsForGame(); - - if (FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) || - WantsToCloseMenu()) - { - ReturnToMainWindow(); - } - - const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); - text = Achievements::GetGameTitle(); - - if (Achievements::ChallengeModeActive()) - text += FSUI_VSTR(" (Hardcore Mode)"); - - top += g_large_font->FontSize + spacing; - - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.c_str(), text.c_str() + text.length(), nullptr, - ImVec2(0.0f, 0.0f), &title_bb); - ImGui::PopFont(); - - const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); - if (unlocked_count == achievement_count) - { - text = fmt::format(FSUI_FSTR("You have unlocked all achievements and earned {} points!"), total_points); - } - else - { - text = fmt::format(FSUI_FSTR("You have unlocked {} of {} achievements, earning {} of {} possible points."), - unlocked_count, achievement_count, current_points, total_points); - } - - top += g_medium_font->FontSize + spacing; - - ImGui::PushFont(g_medium_font); - ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.c_str(), text.c_str() + text.length(), nullptr, - ImVec2(0.0f, 0.0f), &summary_bb); - ImGui::PopFont(); - - const float progress_height = LayoutScale(20.0f); - const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height)); - const float fraction = static_cast(unlocked_count) / static_cast(achievement_count); - dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor)); - dl->AddRectFilled(progress_bb.Min, - ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), - ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor)); - - text = fmt::format("{}%", static_cast(std::round(fraction * 100.0f))); - text_size = ImGui::CalcTextSize(text.c_str()); - const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), - progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - - (text_size.y / 2.0f)); - dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, - ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), text.c_str(), text.c_str() + text.length()); - top += progress_height + spacing; - } - } - EndFullscreenWindow(); - - ImGui::SetNextWindowBgAlpha(alpha); - - if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height), - "achievements", background, 0.0f, 0.0f, 0)) - { - BeginMenuButtons(); - - static bool unlocked_achievements_collapsed = false; - - unlocked_achievements_collapsed ^= MenuHeadingButton( - FSUI_CSTR("Unlocked Achievements"), unlocked_achievements_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP); - if (!unlocked_achievements_collapsed) - { - Achievements::EnumerateAchievements([](const Achievements::Achievement& cheevo) -> bool { - if (!cheevo.locked) - DrawAchievement(cheevo); - - return true; - }); - } - - if (Achievements::GetUnlockedAchiementCount() != Achievements::GetAchievementCount()) - { - static bool locked_achievements_collapsed = false; - locked_achievements_collapsed ^= MenuHeadingButton( - FSUI_CSTR("Locked Achievements"), locked_achievements_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP); - if (!locked_achievements_collapsed) - { - Achievements::EnumerateAchievements([](const Achievements::Achievement& cheevo) -> bool { - if (cheevo.locked) - DrawAchievement(cheevo); - - return true; - }); - } - } - - EndMenuButtons(); - } - EndFullscreenWindow(); -} - -void FullscreenUI::DrawPrimedAchievementsIcons() -{ - const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); - const float spacing = LayoutScale(10.0f); - const float padding = LayoutScale(10.0f); - - const ImGuiIO& io = ImGui::GetIO(); - const float x_advance = image_size.x + spacing; - ImVec2 position(io.DisplaySize.x - padding - image_size.x, io.DisplaySize.y - padding - image_size.y); - - auto lock = Achievements::GetLock(); - Achievements::EnumerateAchievements( - [&image_size, &x_advance, &position](const Achievements::Achievement& achievement) { - if (!achievement.primed) - return true; - - const std::string& badge_path = Achievements::GetAchievementBadgePath(achievement, true, true); - if (badge_path.empty()) - return true; - - GPUTexture* badge = GetCachedTextureAsync(badge_path.c_str()); - if (!badge) - return true; - - ImDrawList* dl = ImGui::GetBackgroundDrawList(); - dl->AddImage(badge, position, position + image_size); - position.x -= x_advance; - return true; - }); -} - -void FullscreenUI::DrawPrimedAchievementsList() -{ - auto lock = Achievements::GetLock(); - const u32 primed_count = Achievements::GetPrimedAchievementCount(); - - const ImGuiIO& io = ImGui::GetIO(); - ImFont* font = g_medium_font; - - const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)); - const float margin = LayoutScale(10.0f); - const float spacing = LayoutScale(10.0f); - const float padding = LayoutScale(10.0f); - - const float max_text_width = LayoutScale(300.0f); - const float row_width = max_text_width + padding + padding + image_size.x + spacing; - const float title_height = padding + font->FontSize + padding; - const ImVec2 box_min(io.DisplaySize.x - row_width - margin, margin); - const ImVec2 box_max(box_min.x + row_width, - box_min.y + title_height + (static_cast(primed_count) * (image_size.y + padding))); - - ImDrawList* dl = ImGui::GetBackgroundDrawList(); - dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f)); - dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255), - FSUI_CSTR("Active Challenge Achievements")); - - const float y_advance = image_size.y + spacing; - const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f; - const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f); - ImVec2 position(box_min.x + padding, box_min.y + title_height); - - Achievements::EnumerateAchievements([font, &image_size, max_text_width, spacing, y_advance, acheivement_name_offset, - max_non_ellipised_text_width, - &position](const Achievements::Achievement& achievement) { - if (!achievement.primed) - return true; - - const std::string& badge_path = Achievements::GetAchievementBadgePath(achievement, true, true); - if (badge_path.empty()) - return true; - - GPUTexture* badge = GetCachedTextureAsync(badge_path.c_str()); - if (!badge) - return true; - - ImDrawList* dl = ImGui::GetBackgroundDrawList(); - dl->AddImage(badge, position, position + image_size); - - const char* achievement_title = achievement.title.c_str(); - const char* achievement_tile_end = achievement_title + achievement.title.length(); - const char* remaining_text = nullptr; - const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title, - achievement_tile_end, &remaining_text)); - const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset); - const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width, - text_position.y + image_size.y); - const u32 text_color = IM_COL32(255, 255, 255, 255); - - if (remaining_text < achievement_tile_end) - { - dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f, &text_bbox); - dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...", - nullptr, 0.0f, &text_bbox); - } - else - { - dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, - achievement_title + achievement.title.length(), 0.0f, &text_bbox); - } - - position.y += y_advance; - return true; - }); + return (s_current_main_window == MainWindowType::Achievements); } bool FullscreenUI::OpenLeaderboardsWindow() { - if (!System::IsValid() || !Achievements::HasActiveGame() || Achievements::GetLeaderboardCount() == 0 || !Initialize()) + if (!Achievements::HasLeaderboards() || !Initialize() || !Achievements::PrepareLeaderboardsWindow()) return false; if (s_current_main_window != MainWindowType::PauseMenu) PauseForMenuOpen(); s_current_main_window = MainWindowType::Leaderboards; - s_open_leaderboard_id.reset(); QueueResetFocus(); return true; } -void FullscreenUI::DrawLeaderboardListEntry(const Achievements::Leaderboard& lboard) +bool FullscreenUI::IsLeaderboardsWindowOpen() { - static constexpr float alpha = 0.8f; - - TinyString id_str; - id_str.Format("%u", lboard.id); - - ImRect bb; - bool visible, hovered; - bool pressed = - MenuButtonFrame(id_str, true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); - if (!visible) - return; - - const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); - const float text_start_x = bb.Min.x + LayoutScale(15.0f); - const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); - - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, lboard.title.c_str(), lboard.title.c_str() + lboard.title.size(), - nullptr, ImVec2(0.0f, 0.0f), &title_bb); - ImGui::PopFont(); - - if (!lboard.description.empty()) - { - ImGui::PushFont(g_medium_font); - ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, lboard.description.c_str(), - lboard.description.c_str() + lboard.description.size(), nullptr, ImVec2(0.0f, 0.0f), - &summary_bb); - ImGui::PopFont(); - } - - if (pressed) - { - s_open_leaderboard_id = lboard.id; - } + return (s_current_main_window == MainWindowType::Leaderboards); } -void FullscreenUI::DrawLeaderboardEntry(const Achievements::LeaderboardEntry& lbEntry, float rank_column_width, - float name_column_width, float column_spacing) -{ - static constexpr float alpha = 0.8f; - - ImRect bb; - bool visible, hovered; - bool pressed = MenuButtonFrame(lbEntry.user.c_str(), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, - &bb.Min, &bb.Max, 0, alpha); - if (!visible) - return; - - const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); - float text_start_x = bb.Min.x + LayoutScale(15.0f); - SmallString text; - - text.Format("%u", lbEntry.rank); - - ImGui::PushFont(g_large_font); - if (lbEntry.is_self) - { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(255, 242, 0, 255)); - } - - const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), - nullptr, ImVec2(0.0f, 0.0f), &rank_bb); - text_start_x += rank_column_width + column_spacing; - - const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, lbEntry.user.c_str(), lbEntry.user.c_str() + lbEntry.user.size(), - nullptr, ImVec2(0.0f, 0.0f), &user_bb); - text_start_x += name_column_width + column_spacing; - - const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, lbEntry.formatted_score.c_str(), - lbEntry.formatted_score.c_str() + lbEntry.formatted_score.size(), nullptr, - ImVec2(0.0f, 0.0f), &score_bb); - - if (lbEntry.is_self) - { - ImGui::PopStyleColor(); - } - - ImGui::PopFont(); - - // This API DOES list the submission date/time, but is it relevant? -#if 0 - if (!cheevo.locked) - { - ImGui::PushFont(g_medium_font); - - const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), - ImVec2(bb.Max.x, bb.Min.y + g_medium_font->FontSize + LayoutScale(4.0f))); - text.Format("Unlocked 21 Feb, 2019 @ 3:14am"); - ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), - nullptr, ImVec2(1.0f, 0.0f), &time_bb); - ImGui::PopFont(); - } -#endif - - if (pressed) - { - // Anything? - } -} - -void FullscreenUI::DrawLeaderboardsWindow() -{ - static constexpr float alpha = 0.8f; - static constexpr float heading_height_unscaled = 110.0f; - - // ensure image downloads still happen while we're paused - Achievements::ProcessPendingHTTPRequests(); - - ImGui::SetNextWindowBgAlpha(alpha); - - const bool is_leaderboard_open = s_open_leaderboard_id.has_value(); - bool close_leaderboard_on_exit = false; - - const ImVec4 background(0.13f, 0.13f, 0.13f, alpha); - const ImVec2 display_size(ImGui::GetIO().DisplaySize); - const float padding = LayoutScale(10.0f); - const float spacing = LayoutScale(10.0f); - const float spacing_small = spacing / 2.0f; - float heading_height = LayoutScale(heading_height_unscaled); - if (is_leaderboard_open) - { - // Add space for a legend - spacing + 1 line of text + spacing + line - heading_height += spacing + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing; - } - - const float rank_column_width = - g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, "99999").x; - const float name_column_width = - g_large_font - ->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, "WWWWWWWWWWWWWWWWWWWW") - .x; - const float column_spacing = spacing * 2.0f; - - if (BeginFullscreenWindow( - ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "leaderboards_heading", background, 0.0f, 0.0f, - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) - { - ImRect bb; - bool visible, hovered; - bool pressed = MenuButtonFrame("leaderboards_heading", false, heading_height_unscaled, &visible, &hovered, &bb.Min, - &bb.Max, 0, alpha); - UNREFERENCED_VARIABLE(pressed); - - if (visible) - { - const float image_height = LayoutScale(85.0f); - - const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); - const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); - - const std::string& icon_path = Achievements::GetGameIcon(); - if (!icon_path.empty()) - { - GPUTexture* badge = GetCachedTexture(icon_path.c_str()); - if (badge) - { - ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), - IM_COL32(255, 255, 255, 255)); - } - } - - float left = bb.Min.x + padding + image_height + spacing; - float right = bb.Max.x - padding; - float top = bb.Min.y + padding; - SmallString text; - - const u32 leaderboard_count = Achievements::GetLeaderboardCount(); - - if (!is_leaderboard_open) - { - if (FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) || - WantsToCloseMenu()) - { - ReturnToMainWindow(); - } - } - else - { - if (FloatingButton(ICON_FA_CARET_SQUARE_LEFT, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) || - WantsToCloseMenu()) - { - close_leaderboard_on_exit = true; - } - } - - const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); - text.Assign(Achievements::GetGameTitle()); - - top += g_large_font->FontSize + spacing; - - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), - nullptr, ImVec2(0.0f, 0.0f), &title_bb); - ImGui::PopFont(); - - if (s_open_leaderboard_id.has_value()) - { - const Achievements::Leaderboard* lboard = Achievements::GetLeaderboardByID(s_open_leaderboard_id.value()); - if (lboard != nullptr) - { - const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); - text.Assign(lboard->title); - - top += g_large_font->FontSize + spacing_small; - - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(subtitle_bb.Min, subtitle_bb.Max, text.GetCharArray(), - text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &subtitle_bb); - ImGui::PopFont(); - - text.Assign(lboard->description); - } - else - { - text.Clear(); - } - } - else - { - text.Fmt(TRANSLATE_FS("Achievements", "This game has {} leaderboards."), leaderboard_count); - } - - const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); - top += g_medium_font->FontSize + spacing_small; - - ImGui::PushFont(g_medium_font); - ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(), - text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb); - - if (!Achievements::ChallengeModeActive()) - { - const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); - top += g_medium_font->FontSize + spacing_small; - - ImGui::RenderTextClipped( - hardcore_warning_bb.Min, hardcore_warning_bb.Max, - TRANSLATE("Achievements", - "Submitting scores is disabled because hardcore mode is off. Leaderboards are read-only."), - nullptr, nullptr, ImVec2(0.0f, 0.0f), &hardcore_warning_bb); - } - - ImGui::PopFont(); - } - - if (is_leaderboard_open) - { - pressed = MenuButtonFrame("legend", false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb.Min, - &bb.Max, 0, alpha); - - UNREFERENCED_VARIABLE(pressed); - - if (visible) - { - const Achievements::Leaderboard* lboard = Achievements::GetLeaderboardByID(s_open_leaderboard_id.value()); - - const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); - float text_start_x = bb.Min.x + LayoutScale(15.0f) + padding; - - ImGui::PushFont(g_large_font); - - const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, "Rank", nullptr, nullptr, ImVec2(0.0f, 0.0f), &rank_bb); - text_start_x += rank_column_width + column_spacing; - - const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, "Name", nullptr, nullptr, ImVec2(0.0f, 0.0f), &user_bb); - text_start_x += name_column_width + column_spacing; - - const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, - lboard != nullptr && Achievements::IsLeaderboardTimeType(*lboard) ? - TRANSLATE("Achievements", "Time") : - TRANSLATE("Achievements", "Score"), - nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb); - - ImGui::PopFont(); - - const float line_thickness = LayoutScale(1.0f); - const float line_padding = LayoutScale(5.0f); - const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); - const ImVec2 line_end(bb.Max.x, line_start.y); - ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), - line_thickness); - } - } - } - EndFullscreenWindow(); - - ImGui::SetNextWindowBgAlpha(alpha); - - if (!is_leaderboard_open) - { - if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height), - "leaderboards", background, 0.0f, 0.0f, 0)) - { - BeginMenuButtons(); - - Achievements::EnumerateLeaderboards([](const Achievements::Leaderboard& lboard) -> bool { - DrawLeaderboardListEntry(lboard); - - return true; - }); - - EndMenuButtons(); - } - EndFullscreenWindow(); - } - else - { - if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height), - "leaderboard", background, 0.0f, 0.0f, 0)) - { - BeginMenuButtons(); - - const auto result = Achievements::TryEnumerateLeaderboardEntries( - s_open_leaderboard_id.value(), - [rank_column_width, name_column_width, column_spacing](const Achievements::LeaderboardEntry& lbEntry) -> bool { - DrawLeaderboardEntry(lbEntry, rank_column_width, name_column_width, column_spacing); - return true; - }); - - if (!result.has_value()) - { - ImGui::PushFont(g_large_font); - - const ImVec2 pos_min(0.0f, heading_height); - const ImVec2 pos_max(display_size.x, display_size.y); - ImGui::RenderTextClipped(pos_min, pos_max, - TRANSLATE("Achievements", "Downloading leaderboard data, please wait..."), nullptr, - nullptr, ImVec2(0.5f, 0.5f)); - - ImGui::PopFont(); - } - - EndMenuButtons(); - } - EndFullscreenWindow(); - } - - if (close_leaderboard_on_exit) - s_open_leaderboard_id.reset(); -} - -#else - -bool FullscreenUI::OpenAchievementsWindow() -{ - return false; -} - -bool FullscreenUI::OpenLeaderboardsWindow() -{ - return false; -} - -#endif - FullscreenUI::ProgressCallback::ProgressCallback(std::string name) : BaseProgressCallback(), m_name(std::move(name)) { ImGuiFullscreen::OpenBackgroundProgressDialog(m_name.c_str(), "", 0, 100, 0); diff --git a/src/core/fullscreen_ui.h b/src/core/fullscreen_ui.h index b32f71fc0..83cd3d8cc 100644 --- a/src/core/fullscreen_ui.h +++ b/src/core/fullscreen_ui.h @@ -7,7 +7,7 @@ #include #include -class GPUTexture; +class String; struct Settings; @@ -23,11 +23,15 @@ void OnSystemDestroyed(); void OnRunningGameChanged(); void OpenPauseMenu(); bool OpenAchievementsWindow(); +bool IsAchievementsWindowOpen(); bool OpenLeaderboardsWindow(); +bool IsLeaderboardsWindowOpen(); +void ReturnToMainWindow(); void Shutdown(); void Render(); void InvalidateCoverCache(); +void TimeToPrintableString(String* str, time_t t); // Returns true if the message has been dismissed. bool DrawErrorWindow(const char* message); diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index c5363ba12..f25bbbe16 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -160,7 +160,7 @@ DEFINE_HOTKEY("Screenshot", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_NOOP System::SaveScreenshot(); }) -#if !defined(__ANDROID__) && defined(WITH_CHEEVOS) +#if !defined(__ANDROID__) DEFINE_HOTKEY("OpenAchievements", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_NOOP("Hotkeys", "Open Achievement List"), [](s32 pressed) { if (!pressed) @@ -184,7 +184,7 @@ DEFINE_HOTKEY("OpenLeaderboards", TRANSLATE_NOOP("Hotkeys", "General"), } } }) -#endif // !defined(__ANDROID__) && defined(WITH_CHEEVOS) +#endif // !defined(__ANDROID__) DEFINE_HOTKEY("Reset", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Reset System"), [](s32 pressed) { if (!pressed) diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 1d213e484..20b16c99c 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -365,16 +365,20 @@ void Settings::Load(SettingsInterface& si) memory_card_use_playlist_title = si.GetBoolValue("MemoryCards", "UsePlaylistTitle", true); achievements_enabled = si.GetBoolValue("Cheevos", "Enabled", false); - achievements_test_mode = si.GetBoolValue("Cheevos", "TestMode", false); - achievements_unofficial_test_mode = si.GetBoolValue("Cheevos", "UnofficialTestMode", false); - achievements_use_first_disc_from_playlist = si.GetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", true); - achievements_rich_presence = si.GetBoolValue("Cheevos", "RichPresence", true); - achievements_challenge_mode = si.GetBoolValue("Cheevos", "ChallengeMode", false); - achievements_leaderboards = si.GetBoolValue("Cheevos", "Leaderboards", true); + achievements_hardcore_mode = si.GetBoolValue("Cheevos", "ChallengeMode", false); achievements_notifications = si.GetBoolValue("Cheevos", "Notifications", true); + achievements_leaderboard_notifications = si.GetBoolValue("Cheevos", "LeaderboardNotifications", true); achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true); - achievements_primed_indicators = si.GetBoolValue("Cheevos", "PrimedIndicators", true); + achievements_overlays = si.GetBoolValue("Cheevos", "Overlays", true); + achievements_encore_mode = si.GetBoolValue("Cheevos", "EncoreMode", false); + achievements_spectator_mode = si.GetBoolValue("Cheevos", "SpectatorMode", false); + achievements_unofficial_test_mode = si.GetBoolValue("Cheevos", "UnofficialTestMode", false); + achievements_use_first_disc_from_playlist = si.GetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", true); achievements_use_raintegration = si.GetBoolValue("Cheevos", "UseRAIntegration", false); + achievements_notification_duration = + si.GetFloatValue("Cheevos", "NotificationsDuration", DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME); + achievements_leaderboard_duration = + si.GetFloatValue("Cheevos", "LeaderboardsDuration", DEFAULT_LEADERBOARD_NOTIFICATION_TIME); log_level = ParseLogLevelName(si.GetStringValue("Logging", "LogLevel", GetLogLevelName(DEFAULT_LOG_LEVEL)).c_str()) .value_or(DEFAULT_LOG_LEVEL); @@ -564,16 +568,18 @@ void Settings::Save(SettingsInterface& si) const si.SetStringValue("ControllerPorts", "MultitapMode", GetMultitapModeName(multitap_mode)); si.SetBoolValue("Cheevos", "Enabled", achievements_enabled); - si.SetBoolValue("Cheevos", "TestMode", achievements_test_mode); - si.SetBoolValue("Cheevos", "UnofficialTestMode", achievements_unofficial_test_mode); - si.SetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", achievements_use_first_disc_from_playlist); - si.SetBoolValue("Cheevos", "RichPresence", achievements_rich_presence); - si.SetBoolValue("Cheevos", "ChallengeMode", achievements_challenge_mode); - si.SetBoolValue("Cheevos", "Leaderboards", achievements_leaderboards); + si.SetBoolValue("Cheevos", "ChallengeMode", achievements_hardcore_mode); si.SetBoolValue("Cheevos", "Notifications", achievements_notifications); + si.SetBoolValue("Cheevos", "LeaderboardNotifications", achievements_leaderboard_notifications); si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects); - si.SetBoolValue("Cheevos", "PrimedIndicators", achievements_primed_indicators); + si.SetBoolValue("Cheevos", "Overlays", achievements_overlays); + si.SetBoolValue("Cheevos", "EncoreMode", achievements_encore_mode); + si.SetBoolValue("Cheevos", "SpectatorMode", achievements_spectator_mode); + si.SetBoolValue("Cheevos", "UnofficialTestMode", achievements_unofficial_test_mode); + si.SetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", achievements_use_first_disc_from_playlist); si.SetBoolValue("Cheevos", "UseRAIntegration", achievements_use_raintegration); + si.SetFloatValue("Cheevos", "NotificationsDuration", achievements_notification_duration); + si.SetFloatValue("Cheevos", "LeaderboardsDuration", achievements_leaderboard_duration); si.SetStringValue("Logging", "LogLevel", GetLogLevelName(log_level)); si.SetStringValue("Logging", "LogFilter", log_filter.c_str()); @@ -696,7 +702,7 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages) } // if challenge mode is enabled, disable things like rewind since they use save states - if (Achievements::ChallengeModeActive()) + if (Achievements::IsHardcoreModeActive()) { g_settings.emulation_speed = (g_settings.emulation_speed != 0.0f) ? std::max(g_settings.emulation_speed, 1.0f) : 0.0f; diff --git a/src/core/settings.h b/src/core/settings.h index d5060a4b4..843e55eb0 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -180,16 +180,18 @@ struct Settings // achievements bool achievements_enabled = false; - bool achievements_test_mode = false; - bool achievements_unofficial_test_mode = false; - bool achievements_use_first_disc_from_playlist = true; - bool achievements_rich_presence = true; - bool achievements_challenge_mode = false; - bool achievements_leaderboards = true; + bool achievements_hardcore_mode = false; bool achievements_notifications = true; + bool achievements_leaderboard_notifications = true; bool achievements_sound_effects = true; - bool achievements_primed_indicators = true; + bool achievements_overlays = true; + bool achievements_encore_mode = false; + bool achievements_spectator_mode = false; + bool achievements_unofficial_test_mode = false; + bool achievements_use_first_disc_from_playlist = true; bool achievements_use_raintegration = false; + float achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME; + float achievements_leaderboard_duration = DEFAULT_LEADERBOARD_NOTIFICATION_TIME; struct DebugSettings { @@ -473,6 +475,9 @@ struct Settings static constexpr MemoryCardType DEFAULT_MEMORY_CARD_2_TYPE = MemoryCardType::None; static constexpr MultitapMode DEFAULT_MULTITAP_MODE = MultitapMode::Disabled; + static constexpr float DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME = 10.0f; + static constexpr float DEFAULT_LEADERBOARD_NOTIFICATION_TIME = 10.0f; + static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO; #ifndef __ANDROID__ diff --git a/src/core/system.cpp b/src/core/system.cpp index dcbd6d6ab..8eb303ec8 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -135,7 +135,6 @@ static void SetTimerResolutionIncreased(bool enabled); #ifdef WITH_DISCORD_PRESENCE static void InitializeDiscordPresence(); static void ShutdownDiscordPresence(); -static void UpdateDiscordPresence(bool rich_presence_only); static void PollDiscordPresence(); #endif } // namespace System @@ -233,11 +232,7 @@ static u32 s_runahead_replay_frames = 0; static u64 s_session_start_time = 0; #ifdef WITH_DISCORD_PRESENCE -// discord rich presence static bool s_discord_presence_active = false; -#ifdef WITH_CHEEVOS -static std::string s_discord_presence_cheevos_string; -#endif #endif static TinyString GetTimestampStringForFileName() @@ -250,14 +245,12 @@ void System::Internal::ProcessStartup() // This will call back to Host::LoadSettings() -> ReloadSources(). LoadSettings(false); -#ifdef WITH_CHEEVOS #ifdef WITH_RAINTEGRATION if (Host::GetBaseBoolSettingValue("Cheevos", "UseRAIntegration", false)) Achievements::SwitchToRAIntegration(); #endif if (g_settings.achievements_enabled) Achievements::Initialize(); -#endif } void System::Internal::ProcessShutdown() @@ -266,9 +259,7 @@ void System::Internal::ProcessShutdown() ShutdownDiscordPresence(); #endif -#ifdef WITH_CHEEVOS - Achievements::Shutdown(); -#endif + Achievements::Shutdown(false); InputManager::CloseSources(); } @@ -281,9 +272,7 @@ void System::Internal::IdlePollUpdate() PollDiscordPresence(); #endif -#ifdef WITH_CHEEVOS - Achievements::ProcessPendingHTTPRequests(); -#endif + Achievements::IdleUpdate(); } System::State System::GetState() @@ -1068,13 +1057,11 @@ void System::ResetSystem() if (!IsValid()) return; -#ifdef WITH_CHEEVOS if (!Achievements::ConfirmSystemReset()) return; - if (Achievements::ResetChallengeMode()) + if (Achievements::ResetHardcoreMode()) ApplySettings(false); -#endif InternalReset(); ResetPerformanceCounters(); @@ -1096,9 +1083,7 @@ void System::PauseSystem(bool paused) InputManager::PauseVibration(); -#ifdef WITH_CHEEVOS Achievements::OnSystemPaused(true); -#endif if (g_settings.inhibit_screensaver) PlatformMisc::ResumeScreensaver(); @@ -1110,9 +1095,7 @@ void System::PauseSystem(bool paused) { FullscreenUI::OnSystemResumed(); -#ifdef WITH_CHEEVOS Achievements::OnSystemPaused(false); -#endif if (g_settings.inhibit_screensaver) PlatformMisc::SuspendScreensaver(); @@ -1131,13 +1114,11 @@ bool System::LoadState(const char* filename) if (!IsValid()) return false; -#ifdef WITH_CHEEVOS - if (Achievements::ChallengeModeActive() && - !Achievements::ConfirmChallengeModeDisable(TRANSLATE("Achievements", "Loading state"))) + if (Achievements::IsHardcoreModeActive() && + !Achievements::ConfirmHardcoreModeDisable(TRANSLATE("Achievements", "Loading state"))) { return false; } -#endif Common::Timer load_timer; @@ -1265,11 +1246,11 @@ bool System::BootSystem(SystemBootParameters parameters) (do_exe_boot ? GetRegionForExe(parameters.filename.c_str()) : GetRegionForPsf(parameters.filename.c_str())); Log_InfoPrintf("EXE/PSF Region: %s", Settings::GetDiscRegionDisplayName(file_region)); s_region = GetConsoleRegionForDiscRegion(file_region); - if (do_psf_boot) - psf_boot = std::move(parameters.filename); - else - exe_boot = std::move(parameters.filename); } + if (do_psf_boot) + psf_boot = std::move(parameters.filename); + else + exe_boot = std::move(parameters.filename); } else { @@ -1350,17 +1331,15 @@ bool System::BootSystem(SystemBootParameters parameters) return false; } -#ifdef WITH_CHEEVOS // Check for resuming with hardcore mode. - if (!parameters.save_state.empty() && Achievements::ChallengeModeActive() && - !Achievements::ConfirmChallengeModeDisable(TRANSLATE("Achievements", "Resuming state"))) + if (!parameters.save_state.empty() && Achievements::IsHardcoreModeActive() && + !Achievements::ConfirmHardcoreModeDisable(TRANSLATE("Achievements", "Resuming state"))) { s_state = State::Shutdown; ClearRunningGame(); Host::OnSystemDestroyed(); return false; } -#endif // Load BIOS image. if (!LoadBIOS(parameters.override_bios)) @@ -1683,12 +1662,10 @@ void System::ClearRunningGame() Host::OnGameChanged(s_running_game_path, s_running_game_serial, s_running_game_title); -#ifdef WITH_CHEEVOS Achievements::GameChanged(s_running_game_path, nullptr); -#endif #ifdef WITH_DISCORD_PRESENCE - UpdateDiscordPresence(false); + UpdateDiscordPresence(); #endif } @@ -1760,10 +1737,8 @@ void System::FrameDone() if (s_cheat_list) s_cheat_list->Apply(); -#ifdef WITH_CHEEVOS if (Achievements::IsActive()) Achievements::FrameUpdate(); -#endif #ifdef WITH_DISCORD_PRESENCE PollDiscordPresence(); @@ -2131,23 +2106,13 @@ bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di if (!sw.DoMarker("Cheevos")) return false; -#ifdef WITH_CHEEVOS if (!Achievements::DoState(sw)) return false; -#else - // if we compiled without cheevos, we need to toss out the data from states which were - u32 data_size = 0; - sw.Do(&data_size); - if (data_size > 0) - sw.SkipBytes(data_size); -#endif } else { -#ifdef WITH_CHEEVOS // loading an old state without cheevos, so reset the runtime - Achievements::ResetRuntime(); -#endif + Achievements::ResetClient(); } } @@ -2208,9 +2173,7 @@ void System::InternalReset() TimingEvents::Reset(); ResetPerformanceCounters(); -#ifdef WITH_CHEEVOS - Achievements::ResetRuntime(); -#endif + Achievements::ResetClient(); } std::string System::GetMediaPathFromSaveState(const char* path) @@ -2352,15 +2315,13 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display, bool ig ClearMemorySaveStates(); -#ifdef WITH_CHEEVOS // Updating game/loading settings can turn on hardcore mode. Catch this. - if (Achievements::ChallengeModeActive()) + if (Achievements::IsHardcoreModeActive()) { Host::AddKeyedOSDMessage("challenge_mode_reset", TRANSLATE_STR("Achievements", "Hardcore mode disabled by state switch."), 10.0f); - Achievements::DisableChallengeMode(); + Achievements::DisableHardcoreMode(); } -#endif if (!state->SeekAbsolute(header.offset_to_data)) return false; @@ -2720,10 +2681,8 @@ void System::SetRewindState(bool enabled) return; } -#ifdef WITH_CHEEVOS - if (Achievements::ChallengeModeActive() && !Achievements::ConfirmChallengeModeDisable("Rewinding")) + if (Achievements::IsHardcoreModeActive() && !Achievements::ConfirmHardcoreModeDisable("Rewinding")) return; -#endif System::SetRewinding(enabled); UpdateSpeedLimiterState(); @@ -2734,10 +2693,8 @@ void System::DoFrameStep() if (!IsValid()) return; -#ifdef WITH_CHEEVOS - if (Achievements::ChallengeModeActive() && !Achievements::ConfirmChallengeModeDisable("Frame stepping")) + if (Achievements::IsHardcoreModeActive() && !Achievements::ConfirmHardcoreModeDisable("Frame stepping")) return; -#endif s_frame_step_request = true; PauseSystem(false); @@ -2748,10 +2705,8 @@ void System::DoToggleCheats() if (!System::IsValid()) return; -#ifdef WITH_CHEEVOS - if (Achievements::ChallengeModeActive() && !Achievements::ConfirmChallengeModeDisable("Toggling cheats")) + if (Achievements::IsHardcoreModeActive() && !Achievements::ConfirmHardcoreModeDisable("Toggling cheats")) return; -#endif CheatList* cl = GetCheatList(); if (!cl) @@ -3341,18 +3296,16 @@ void System::UpdateRunningGame(const char* path, CDImage* image, bool booting) g_texture_replacements.SetGameID(s_running_game_serial); -#ifdef WITH_CHEEVOS if (booting) - Achievements::ResetChallengeMode(); + Achievements::ResetHardcoreMode(); Achievements::GameChanged(s_running_game_path, image); -#endif UpdateGameSettingsLayer(); ApplySettings(true); s_cheat_list.reset(); - if (g_settings.auto_load_cheats && !Achievements::ChallengeModeActive()) + if (g_settings.auto_load_cheats && !Achievements::IsHardcoreModeActive()) LoadCheatListFromGameTitle(); if (s_running_game_serial != prev_serial) @@ -3361,7 +3314,7 @@ void System::UpdateRunningGame(const char* path, CDImage* image, bool booting) SaveStateSelectorUI::RefreshList(); #ifdef WITH_DISCORD_PRESENCE - UpdateDiscordPresence(false); + UpdateDiscordPresence(); #endif Host::OnGameChanged(s_running_game_path, s_running_game_serial, s_running_game_title); @@ -3732,9 +3685,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings) if (g_settings.multitap_mode != old_settings.multitap_mode) UpdateMultitaps(); -#ifdef WITH_CHEEVOS Achievements::UpdateSettings(old_settings); -#endif FullscreenUI::CheckForConfigChanges(old_settings); @@ -4417,7 +4368,7 @@ bool System::LoadCheatList(const char* filename) bool System::LoadCheatListFromGameTitle() { // Called when booting, needs to test for shutdown. - if (IsShutdown() || Achievements::ChallengeModeActive()) + if (IsShutdown() || Achievements::IsHardcoreModeActive()) return false; const std::string filename(GetCheatFileName()); @@ -4429,7 +4380,7 @@ bool System::LoadCheatListFromGameTitle() bool System::LoadCheatListFromDatabase() { - if (IsShutdown() || s_running_game_serial.empty() || Achievements::ChallengeModeActive()) + if (IsShutdown() || s_running_game_serial.empty() || Achievements::IsHardcoreModeActive()) return false; std::unique_ptr cl = std::make_unique(); @@ -4775,7 +4726,7 @@ void System::InitializeDiscordPresence() Discord_Initialize("705325712680288296", &handlers, 0, nullptr); s_discord_presence_active = true; - UpdateDiscordPresence(false); + UpdateDiscordPresence(); } void System::ShutdownDiscordPresence() @@ -4786,31 +4737,13 @@ void System::ShutdownDiscordPresence() Discord_ClearPresence(); Discord_Shutdown(); s_discord_presence_active = false; -#ifdef WITH_CHEEVOS - s_discord_presence_cheevos_string.clear(); -#endif } -void System::UpdateDiscordPresence(bool rich_presence_only) +void System::UpdateDiscordPresence() { if (!s_discord_presence_active) return; -#ifdef WITH_CHEEVOS - // Update only if RetroAchievements rich presence has changed - const std::string& new_rich_presence = Achievements::GetRichPresenceString(); - if (new_rich_presence == s_discord_presence_cheevos_string && rich_presence_only) - { - return; - } - s_discord_presence_cheevos_string = new_rich_presence; -#else - if (rich_presence_only) - { - return; - } -#endif - // https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields DiscordRichPresence rp = {}; rp.largeImageKey = "duckstation_logo"; @@ -4827,22 +4760,19 @@ void System::UpdateDiscordPresence(bool rich_presence_only) details_string.AppendString("No Game Running"); } -#ifdef WITH_CHEEVOS SmallString state_string; - // Trim to 128 bytes as per Discord-RPC requirements - if (s_discord_presence_cheevos_string.length() >= 128) - { - // 124 characters + 3 dots + null terminator - state_string = s_discord_presence_cheevos_string.substr(0, 124); - state_string.AppendString("..."); - } - else + + if (Achievements::HasRichPresence()) { - state_string = s_discord_presence_cheevos_string; - } + const auto lock = Achievements::GetLock(); + const std::string_view richp = Achievements::GetRichPresenceString(); + if (richp.length() >= 128) + state_string.AppendFmtString("{}...", richp.substr(0, 124)); + else + state_string.Assign(richp); - rp.state = state_string; -#endif + rp.state = state_string; + } rp.details = details_string; Discord_UpdatePresence(&rp); @@ -4853,8 +4783,6 @@ void System::PollDiscordPresence() if (!s_discord_presence_active) return; - UpdateDiscordPresence(true); - Discord_RunCallbacks(); } diff --git a/src/core/system.h b/src/core/system.h index 49b81da7b..776cb02fe 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -479,6 +479,11 @@ void UpdateMemorySaveStateSettings(); bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true); void SetRunaheadReplayFlag(); +#ifdef WITH_DISCORD_PRESENCE +/// Called when rich presence changes. +void UpdateDiscordPresence(); +#endif + namespace Internal { /// Called on process startup. void ProcessStartup(); diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp index 42a5ea10a..3d833d16e 100644 --- a/src/duckstation-nogui/nogui_host.cpp +++ b/src/duckstation-nogui/nogui_host.cpp @@ -751,11 +751,26 @@ void Host::OnGameChanged(const std::string& disc_path, const std::string& game_s NoGUIHost::UpdateWindowTitle(game_name); } +void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ + // noop +} + +void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) +{ + // noop +} + void Host::OnAchievementsRefreshed() { // noop } +void Host::OnAchievementsHardcoreModeChanged() +{ + // noop +} + void Host::SetMouseMode(bool relative, bool hide_cursor) { #if 0 diff --git a/src/duckstation-qt/achievementlogindialog.cpp b/src/duckstation-qt/achievementlogindialog.cpp index e966a8b3a..6f63060a2 100644 --- a/src/duckstation-qt/achievementlogindialog.cpp +++ b/src/duckstation-qt/achievementlogindialog.cpp @@ -1,16 +1,29 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "achievementlogindialog.h" -#include "core/achievements.h" #include "qthost.h" + +#include "core/achievements.h" + +#include "common/error.h" + #include -AchievementLoginDialog::AchievementLoginDialog(QWidget* parent) : QDialog(parent) +AchievementLoginDialog::AchievementLoginDialog(QWidget* parent, Achievements::LoginRequestReason reason) + : QDialog(parent) { m_ui.setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + // Adjust text if needed based on reason. + if (reason == Achievements::LoginRequestReason::TokenInvalid) + { + m_ui.instructionText->setText(tr("Your RetroAchievements login token is no longer valid. You must " + "re-enter your credentials for achievements to be tracked. Your password will not " + "be saved in DuckStation, an access token will be generated and used instead.")); + } + m_login = m_ui.buttonBox->addButton(tr("&Login"), QDialogButtonBox::AcceptRole); m_login->setEnabled(false); connectUi(); @@ -28,8 +41,11 @@ void AchievementLoginDialog::loginClicked() enableUI(false); Host::RunOnCPUThread([this, username, password]() { - const bool result = Achievements::Login(username.toStdString().c_str(), password.toStdString().c_str()); - QMetaObject::invokeMethod(this, "processLoginResult", Qt::QueuedConnection, Q_ARG(bool, result)); + Error error; + const bool result = Achievements::Login(username.toUtf8().constData(), password.toUtf8().constData(), &error); + const QString message = QString::fromStdString(error.GetDescription()); + QMetaObject::invokeMethod(this, "processLoginResult", Qt::QueuedConnection, Q_ARG(bool, result), + Q_ARG(const QString&, message)); }); } @@ -38,12 +54,13 @@ void AchievementLoginDialog::cancelClicked() done(1); } -void AchievementLoginDialog::processLoginResult(bool result) +void AchievementLoginDialog::processLoginResult(bool result, const QString& message) { if (!result) { - QMessageBox::critical(this, tr("Login Error"), - tr("Login failed. Please check your username and password, and try again.")); + QMessageBox::critical( + this, tr("Login Error"), + tr("Login failed.\nError: %1\n\nPlease check your username and password, and try again.").arg(message)); m_ui.status->setText(tr("Login failed.")); enableUI(true); return; diff --git a/src/duckstation-qt/achievementlogindialog.h b/src/duckstation-qt/achievementlogindialog.h index 9a1319e20..f33225185 100644 --- a/src/duckstation-qt/achievementlogindialog.h +++ b/src/duckstation-qt/achievementlogindialog.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -6,18 +6,22 @@ #include #include +namespace Achievements { +enum class LoginRequestReason; +} + class AchievementLoginDialog : public QDialog { Q_OBJECT public: - AchievementLoginDialog(QWidget* parent); + AchievementLoginDialog(QWidget* parent, Achievements::LoginRequestReason reason); ~AchievementLoginDialog(); private Q_SLOTS: void loginClicked(); void cancelClicked(); - void processLoginResult(bool result); + void processLoginResult(bool result, const QString& message); private: void connectUi(); diff --git a/src/duckstation-qt/achievementlogindialog.ui b/src/duckstation-qt/achievementlogindialog.ui index c17571c81..098017b52 100644 --- a/src/duckstation-qt/achievementlogindialog.ui +++ b/src/duckstation-qt/achievementlogindialog.ui @@ -64,9 +64,9 @@ - + - Please enter user name and password for retroachievements.org below. Your password will not be saved in DuckStation, an access token will be generated and used instead. + Please enter your user name and password for retroachievements.org below. Your password will not be saved in DuckStation, an access token will be generated and used instead. true diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp index aa4dae496..eb7c8bcfe 100644 --- a/src/duckstation-qt/achievementsettingswidget.cpp +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -24,54 +24,64 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsDialog* dialog, QWi m_ui.setupUi(this); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enable, "Cheevos", "Enabled", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.richPresence, "Cheevos", "RichPresence", true); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.challengeMode, "Cheevos", "ChallengeMode", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.testMode, "Cheevos", "TestMode", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialTestMode, "Cheevos", "UnofficialTestMode", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hardcoreMode, "Cheevos", "ChallengeMode", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.achievementNotifications, "Cheevos", "Notifications", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardNotifications, "Cheevos", + "LeaderboardNotifications", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.overlays, "Cheevos", "Overlays", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.encoreMode, "Cheevos", "EncoreMode", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spectatorMode, "Cheevos", "SpectatorMode", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialAchievements, "Cheevos", "UnofficialTestMode", + false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useFirstDiscFromPlaylist, "Cheevos", "UseFirstDiscFromPlaylist", true); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboards, "Cheevos", "Leaderboards", true); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.notifications, "Cheevos", "Notifications", true); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.primedIndicators, "Cheevos", "PrimedIndicators", true); + SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.achievementNotificationsDuration, "Cheevos", + "NotificationsDuration", + Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME); + SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.leaderboardNotificationsDuration, "Cheevos", + "LeaderboardsDuration", + Settings::DEFAULT_LEADERBOARD_NOTIFICATION_TIME); dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"), tr("When enabled and logged in, DuckStation will scan for achievements on startup.")); - dialog->registerWidgetHelp(m_ui.testMode, tr("Enable Test Mode"), tr("Unchecked"), + dialog->registerWidgetHelp(m_ui.hardcoreMode, tr("Enable Hardcore Mode"), tr("Unchecked"), + tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save " + "state, cheats, and slowdown functions.")); + dialog->registerWidgetHelp(m_ui.achievementNotifications, tr("Show Achievement Notifications"), tr("Checked"), + tr("Displays popup messages on events such as achievement unlocks and game completion.")); + dialog->registerWidgetHelp( + m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"), + tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge.")); + dialog->registerWidgetHelp( + m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"), + tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions.")); + dialog->registerWidgetHelp( + m_ui.overlays, tr("Enable In-Game Overlays"), tr("Checked"), + tr("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.")); + dialog->registerWidgetHelp(m_ui.encoreMode, tr("Enable Encore Mode"), tr("Unchecked"), + tr("When enabled, each session will behave as if no achievements have been unlocked.")); + dialog->registerWidgetHelp(m_ui.spectatorMode, tr("Enable Spectator Mode"), tr("Unchecked"), tr("When enabled, DuckStation will assume all achievements are locked and not send any " "unlock notifications to the server.")); dialog->registerWidgetHelp( - m_ui.unofficialTestMode, tr("Test Unofficial Achievements"), tr("Unchecked"), + m_ui.unofficialAchievements, tr("Test Unofficial Achievements"), tr("Unchecked"), tr("When enabled, DuckStation will list achievements from unofficial sets. Please note that these achievements are " "not tracked by RetroAchievements, so they unlock every time.")); - dialog->registerWidgetHelp( - m_ui.richPresence, tr("Enable Rich Presence"), tr("Unchecked"), - tr("When enabled, rich presence information will be collected and sent to the server where supported.")); dialog->registerWidgetHelp( m_ui.useFirstDiscFromPlaylist, tr("Use First Disc From Playlist"), tr("Unchecked"), tr( "When enabled, the first disc in a playlist will be used for achievements, regardless of which disc is active.")); - dialog->registerWidgetHelp(m_ui.challengeMode, tr("Enable Hardcore Mode"), tr("Unchecked"), - tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save " - "state, cheats, and slowdown functions.")); - dialog->registerWidgetHelp( - m_ui.notifications, tr("Show Notifications"), tr("Checked"), - tr("Displays popup messages on events such as achievement unlocks and leaderboard submissions.")); - dialog->registerWidgetHelp( - m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"), - tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions.")); - dialog->registerWidgetHelp( - m_ui.leaderboards, tr("Enable Leaderboards"), tr("Checked"), - tr("Enables tracking and submission of leaderboards in supported games. If leaderboards " - "are disabled, you will still be able to view the leaderboard and scores, but no scores will be uploaded.")); - dialog->registerWidgetHelp( - m_ui.primedIndicators, tr("Show Challenge Indicators"), tr("Checked"), - tr("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.")); connect(m_ui.enable, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); - connect(m_ui.notifications, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); - connect(m_ui.challengeMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); - connect(m_ui.challengeMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::onChallengeModeStateChanged); + connect(m_ui.hardcoreMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); + connect(m_ui.hardcoreMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::onHardcoreModeStateChanged); + connect(m_ui.achievementNotifications, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); + connect(m_ui.leaderboardNotifications, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); + connect(m_ui.achievementNotificationsDuration, &QSlider::valueChanged, this, + &AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged); + connect(m_ui.leaderboardNotificationsDuration, &QSlider::valueChanged, this, + &AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged); if (!m_dialog->isPerGameSettings()) { @@ -95,6 +105,8 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsDialog* dialog, QWi } updateEnableState(); + onAchievementsNotificationDurationSliderChanged(); + onLeaderboardsNotificationDurationSliderChanged(); } AchievementSettingsWidget::~AchievementSettingsWidget() = default; @@ -102,19 +114,24 @@ AchievementSettingsWidget::~AchievementSettingsWidget() = default; void AchievementSettingsWidget::updateEnableState() { const bool enabled = m_dialog->getEffectiveBoolValue("Cheevos", "Enabled", false); - const bool challenge = m_dialog->getEffectiveBoolValue("Cheevos", "ChallengeMode", false); - m_ui.testMode->setEnabled(enabled); - m_ui.useFirstDiscFromPlaylist->setEnabled(enabled); - m_ui.richPresence->setEnabled(enabled); - m_ui.challengeMode->setEnabled(enabled); - m_ui.leaderboards->setEnabled(enabled && challenge); - m_ui.unofficialTestMode->setEnabled(enabled); - m_ui.notifications->setEnabled(enabled); + const bool notifications = enabled && m_dialog->getEffectiveBoolValue("Cheevos", "Notifications", true); + const bool lb_notifications = enabled && m_dialog->getEffectiveBoolValue("Cheevos", "LeaderboardNotifications", true); + m_ui.hardcoreMode->setEnabled(enabled); + m_ui.achievementNotifications->setEnabled(enabled); + m_ui.leaderboardNotifications->setEnabled(enabled); + m_ui.achievementNotificationsDuration->setEnabled(notifications); + m_ui.achievementNotificationsDurationLabel->setEnabled(notifications); + m_ui.leaderboardNotificationsDuration->setEnabled(lb_notifications); + m_ui.leaderboardNotificationsDurationLabel->setEnabled(lb_notifications); m_ui.soundEffects->setEnabled(enabled); - m_ui.primedIndicators->setEnabled(enabled); + m_ui.overlays->setEnabled(enabled); + m_ui.encoreMode->setEnabled(enabled); + m_ui.spectatorMode->setEnabled(enabled); + m_ui.unofficialAchievements->setEnabled(enabled); + m_ui.useFirstDiscFromPlaylist->setEnabled(enabled); } -void AchievementSettingsWidget::onChallengeModeStateChanged() +void AchievementSettingsWidget::onHardcoreModeStateChanged() { if (!QtHost::IsSystemValid()) return; @@ -140,6 +157,20 @@ void AchievementSettingsWidget::onChallengeModeStateChanged() g_emu_thread->resetSystem(); } +void AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged() +{ + const float duration = m_dialog->getEffectiveFloatValue("Cheevos", "NotificationsDuration", + Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME); + m_ui.achievementNotificationsDurationLabel->setText(tr("%n seconds", nullptr, static_cast(duration))); +} + +void AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged() +{ + const float duration = m_dialog->getEffectiveFloatValue("Cheevos", "LeaderboardsDuration", + Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME); + m_ui.leaderboardNotificationsDurationLabel->setText(tr("%n seconds", nullptr, static_cast(duration))); +} + void AchievementSettingsWidget::updateLoginState() { const std::string username(Host::GetBaseStringSettingValue("Cheevos", "Username")); @@ -173,7 +204,7 @@ void AchievementSettingsWidget::onLoginLogoutPressed() return; } - AchievementLoginDialog login(this); + AchievementLoginDialog login(this, Achievements::LoginRequestReason::UserInitiated); int res = login.exec(); if (res != 0) return; @@ -193,8 +224,7 @@ void AchievementSettingsWidget::onViewProfilePressed() QUrl(QStringLiteral("https://retroachievements.org/user/%1").arg(QString::fromUtf8(encoded_username)))); } -void AchievementSettingsWidget::onAchievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, - quint32 points) +void AchievementSettingsWidget::onAchievementsRefreshed(quint32 id, const QString& game_info_string) { m_ui.gameInfo->setText(game_info_string); } diff --git a/src/duckstation-qt/achievementsettingswidget.h b/src/duckstation-qt/achievementsettingswidget.h index 658205aca..a6107a02c 100644 --- a/src/duckstation-qt/achievementsettingswidget.h +++ b/src/duckstation-qt/achievementsettingswidget.h @@ -17,10 +17,12 @@ public: private Q_SLOTS: void updateEnableState(); - void onChallengeModeStateChanged(); + void onHardcoreModeStateChanged(); + void onAchievementsNotificationDurationSliderChanged(); + void onLeaderboardsNotificationDurationSliderChanged(); void onLoginLogoutPressed(); void onViewProfilePressed(); - void onAchievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points); + void onAchievementsRefreshed(quint32 id, const QString& game_info_string); private: void updateLoginState(); diff --git a/src/duckstation-qt/achievementsettingswidget.ui b/src/duckstation-qt/achievementsettingswidget.ui index 20150c36f..e7c65b522 100644 --- a/src/duckstation-qt/achievementsettingswidget.ui +++ b/src/duckstation-qt/achievementsettingswidget.ui @@ -6,8 +6,8 @@ 0 0 - 648 - 475 + 674 + 481 @@ -29,20 +29,13 @@ - Global Settings + Settings - - - - Enable Rich Presence - - - - + - Enable Leaderboards + Enable Spectator Mode @@ -53,52 +46,146 @@ + + + + Test Unofficial Achievements + + + - + - Enable Hardcore Mode + Enable Encore Mode - - + + - Show Challenge Indicators + Enable Hardcore Mode - + Use First Disc From Playlist - - + + + + + + + Notifications + + + + + + + + 3 + + + 30 + + + 1 + + + 5 + + + Qt::Horizontal + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + 5 seconds + + + + + + + - Enable Test Mode + Show Achievement Notifications - - + + + + + + 3 + + + 30 + + + 1 + + + 5 + + + Qt::Horizontal + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + 5 seconds + + + + + + + - Test Unofficial Achievements + Show Leaderboard Notifications - + Enable Sound Effects - - + + - Show Notifications + Enable In-Game Overlays @@ -113,24 +200,28 @@ + + Username: +Login token generated at: + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - + - + - Login... + View Profile... - + - View Profile... + Login... @@ -144,7 +235,7 @@ 0 - 120 + 75 diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index ed573a827..621fb8886 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "mainwindow.h" #include "aboutdialog.h" +#include "achievementlogindialog.h" #include "autoupdaterdialog.h" #include "cheatmanagerdialog.h" #include "coverdownloaddialog.h" @@ -507,14 +508,14 @@ void MainWindow::onSystemStarting() s_system_valid = false; s_system_paused = false; - updateEmulationActions(true, false, Achievements::ChallengeModeActive()); + updateEmulationActions(true, false, Achievements::IsHardcoreModeActive()); } void MainWindow::onSystemStarted() { m_was_disc_change_request = false; s_system_valid = true; - updateEmulationActions(false, true, Achievements::ChallengeModeActive()); + updateEmulationActions(false, true, Achievements::IsHardcoreModeActive()); updateWindowTitle(); updateStatusBarWidgetVisibility(); } @@ -572,7 +573,7 @@ void MainWindow::onSystemDestroyed() return; } - updateEmulationActions(false, false, Achievements::ChallengeModeActive()); + updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); if (m_display_widget) updateDisplayWidgetCursor(); else @@ -724,7 +725,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg { std::vector available_states(System::GetAvailableSaveStates(entry->serial.c_str())); const QString timestamp_format = QLocale::system().dateTimeFormat(QLocale::ShortFormat); - const bool challenge_mode = Achievements::ChallengeModeActive(); + const bool challenge_mode = Achievements::IsHardcoreModeActive(); for (SaveStateInfo& ssi : available_states) { if (ssi.global) @@ -1382,7 +1383,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) g_emu_thread->bootSystem(std::move(boot_params)); }); - if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::ChallengeModeActive()) + if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive()) { connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() { m_open_debugger_on_start = true; @@ -1874,7 +1875,7 @@ void MainWindow::switchToEmulationView() void MainWindow::connectSignals() { - updateEmulationActions(false, false, Achievements::ChallengeModeActive()); + updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); @@ -1980,10 +1981,10 @@ void MainWindow::connectSignals() connect(g_emu_thread, &EmuThread::runningGameChanged, this, &MainWindow::onRunningGameChanged); connect(g_emu_thread, &EmuThread::mouseModeRequested, this, &MainWindow::onMouseModeRequested); connect(g_emu_thread, &EmuThread::fullscreenUIStateChange, this, &MainWindow::onFullscreenUIStateChange); -#ifdef WITH_CHEEVOS + connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); + connect(g_emu_thread, &EmuThread::achievementsLoginSucceeded, this, &MainWindow::onAchievementsLoginSucceeded); connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this, &MainWindow::onAchievementsChallengeModeChanged); -#endif // These need to be queued connections to stop crashing due to menus opening/closing and switching focus. connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress); @@ -2619,10 +2620,28 @@ void MainWindow::openMemoryCardEditor(const QString& card_a_path, const QString& } } +void MainWindow::onAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ + const auto lock = pauseAndLockSystem(); + + AchievementLoginDialog dlg(lock.getDialogParent(), reason); + dlg.exec(); +} + +void MainWindow::onAchievementsLoginSucceeded(const QString& display_name, quint32 points, quint32 sc_points, + quint32 unread_messages) +{ + const QString message = tr("RA: Logged in as %1 (%2, %3 softcore). %4 unread messages.") + .arg(display_name) + .arg(points) + .arg(sc_points) + .arg(unread_messages); + m_ui.statusBar->showMessage(message); +} + void MainWindow::onAchievementsChallengeModeChanged() { -#ifdef WITH_CHEEVOS - const bool active = Achievements::ChallengeModeActive(); + const bool active = Achievements::IsHardcoreModeActive(); if (active) { if (m_cheat_manager_dialog) @@ -2641,7 +2660,6 @@ void MainWindow::onAchievementsChallengeModeChanged() } updateEmulationActions(false, System::IsValid(), active); -#endif } void MainWindow::onToolsMemoryCardEditorTriggered() diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 80b0283fc..ad63c0d42 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -33,6 +33,9 @@ class DebuggerWindow; class MainWindow; class GPUDevice; +namespace Achievements { +enum class LoginRequestReason; +} namespace GameList { struct Entry; } @@ -128,6 +131,9 @@ private Q_SLOTS: void onSystemPaused(); void onSystemResumed(); void onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title); + void onAchievementsLoginRequested(Achievements::LoginRequestReason reason); + void onAchievementsLoginSucceeded(const QString& display_name, quint32 points, quint32 sc_points, + quint32 unread_messages); void onAchievementsChallengeModeChanged(); void onApplicationStateChanged(Qt::ApplicationState state); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index c9c7ea7e4..b74fa4529 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1204,32 +1204,33 @@ void EmuThread::saveScreenshot() System::SaveScreenshot(nullptr, true, true); } +void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ + emit g_emu_thread->achievementsLoginRequested(reason); +} + +void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) +{ + emit g_emu_thread->achievementsLoginSucceeded(QString::fromUtf8(username), points, sc_points, unread_messages); +} + void Host::OnAchievementsRefreshed() { -#ifdef WITH_CHEEVOS u32 game_id = 0; - u32 achievement_count = 0; - u32 max_points = 0; QString game_info; if (Achievements::HasActiveGame()) { game_id = Achievements::GetGameID(); - achievement_count = Achievements::GetAchievementCount(); - max_points = Achievements::GetMaximumPointsForGame(); game_info = qApp - ->translate("EmuThread", "Game ID: %1\n" - "Game Title: %2\n" - "Achievements: %5 (%6)\n\n") - .arg(game_id) - .arg(QString::fromStdString(Achievements::GetGameTitle())) - .arg(achievement_count) - .arg(qApp->translate("EmuThread", "%n points", "", max_points)); - - const std::string rich_presence_string(Achievements::GetRichPresenceString()); - if (!rich_presence_string.empty()) + ->translate("EmuThread", "Game: %1 (%2)\n") + .arg(QString::fromStdString(Achievements::GetGameTitle())) + .arg(game_id); + + const std::string& rich_presence_string = Achievements::GetRichPresenceString(); + if (Achievements::HasRichPresence() && !rich_presence_string.empty()) game_info.append(QString::fromStdString(rich_presence_string)); else game_info.append(qApp->translate("EmuThread", "Rich presence inactive or unsupported.")); @@ -1239,15 +1240,12 @@ void Host::OnAchievementsRefreshed() game_info = qApp->translate("EmuThread", "Game not loaded or no RetroAchievements available."); } - emit g_emu_thread->achievementsRefreshed(game_id, game_info, achievement_count, max_points); -#endif + emit g_emu_thread->achievementsRefreshed(game_id, game_info); } -void Host::OnAchievementsChallengeModeChanged() +void Host::OnAchievementsHardcoreModeChanged() { -#ifdef WITH_CHEEVOS emit g_emu_thread->achievementsChallengeModeChanged(); -#endif } void EmuThread::doBackgroundControllerPoll() diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index e7a564d48..3841f8ded 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -45,14 +45,13 @@ class GPUDevice; class MainWindow; class DisplayWidget; +namespace Achievements { +enum class LoginRequestReason; +} + Q_DECLARE_METATYPE(std::optional); Q_DECLARE_METATYPE(std::shared_ptr); -// These cause errors when compiling with gcc, implicitly defined? -// Q_DECLARE_METATYPE(std::function); -// Q_DECLARE_METATYPE(GPURenderer); -// Q_DECLARE_METATYPE(InputBindingKey); - class EmuThread : public QThread { Q_OBJECT @@ -141,7 +140,10 @@ Q_SIGNALS: void inputProfileLoaded(); void mouseModeRequested(bool relative, bool hide_cursor); void fullscreenUIStateChange(bool running); - void achievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points); + void achievementsLoginRequested(Achievements::LoginRequestReason reason); + void achievementsLoginSucceeded(const QString& display_name, quint32 points, quint32 sc_points, + quint32 unread_messages); + void achievementsRefreshed(quint32 id, const QString& game_info_string); void achievementsChallengeModeChanged(); void cheatEnabled(quint32 index, bool enabled); diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index b85a3a45b..d12b27aaf 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "settingsdialog.h" @@ -7,6 +7,7 @@ #include "biossettingswidget.h" #include "consolesettingswidget.h" +#include "achievementsettingswidget.h" #include "displaysettingswidget.h" #include "emulationsettingswidget.h" #include "enhancementsettingswidget.h" @@ -19,6 +20,7 @@ #include "postprocessingsettingswidget.h" #include "qthost.h" +#include "core/achievements.h" #include "core/host.h" #include "util/ini_settings_interface.h" @@ -29,11 +31,6 @@ #include #include -#ifdef WITH_CHEEVOS -#include "achievementsettingswidget.h" -#include "core/achievements.h" -#endif - static QList s_open_game_properties_dialogs; SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) @@ -129,7 +126,6 @@ void SettingsDialog::addPages() QString help_text(tr("Achievement Settings
These options control RetroAchievements. Mouse over " "an option for additional information.")); -#ifdef WITH_CHEEVOS if (!Achievements::IsUsingRAIntegration()) { addWidget(m_achievement_settings = new AchievementSettingsWidget(this, m_ui.settingsContainer), std::move(title), @@ -144,13 +140,6 @@ void SettingsDialog::addPages() addWidget(placeholder_label, std::move(title), std::move(icon_text), std::move(help_text)); } -#else - QLabel* placeholder_label = - new QLabel(tr("This DuckStation build was not compiled with RetroAchievements support."), m_ui.settingsContainer); - placeholder_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); - - addWidget(placeholder_label, std::move(title), std::move(icon_text), std::move(help_text)); -#endif } if (!isPerGameSettings()) diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 164864b07..0425b5b87 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -325,14 +325,25 @@ void Host::SetMouseMode(bool relative, bool hide_cursor) // } -#ifdef WITH_CHEEVOS +void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ + // noop +} + +void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) +{ + // noop +} void Host::OnAchievementsRefreshed() { // noop } -#endif +void Host::OnAchievementsHardcoreModeChanged() +{ + // noop +} std::optional InputManager::ConvertHostKeyboardStringToCode(const std::string_view& str) { diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp index 9741f7c25..f798b0171 100644 --- a/src/util/imgui_fullscreen.cpp +++ b/src/util/imgui_fullscreen.cpp @@ -15,9 +15,9 @@ #include "common/string_util.h" #include "common/threading.h" #include "common/timer.h" -#include "gpu_device.h" #include "core/host.h" #include "fmt/core.h" +#include "gpu_device.h" #include "imgui_internal.h" #include "imgui_stdlib.h" @@ -140,8 +140,12 @@ static std::string s_file_selector_current_directory; static std::vector s_file_selector_filters; static std::vector s_file_selector_items; +static constexpr float NOTIFICATION_FADE_IN_TIME = 0.2f; +static constexpr float NOTIFICATION_FADE_OUT_TIME = 0.8f; + struct Notification { + std::string key; std::string title; std::string text; std::string badge_path; @@ -272,7 +276,7 @@ std::shared_ptr ImGuiFullscreen::UploadTexture(const char* path, con { std::unique_ptr texture = g_gpu_device->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, GPUTexture::Type::Texture, - GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch()); + GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch()); if (!texture) { Log_ErrorPrintf("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight()); @@ -1614,6 +1618,84 @@ bool ImGuiFullscreen::NavButton(const char* title, bool is_active, bool enabled return pressed; } +bool ImGuiFullscreen::NavTab(const char* title, bool is_active, bool enabled /* = true */, float width, float height, + const ImVec4& background, ImFont* font /* = g_large_font */) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + s_menu_button_index++; + + const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), 0.0f, title)); + const ImVec2 pos(window->DC.CursorPos); + const ImVec2 size = ImVec2(((width < 0.0f) ? text_size.x : LayoutScale(width)), LayoutScale(height)); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + ImGui::ItemSize(ImVec2(size.x, size.y)); + ImGui::SameLine(); + ImGui::PopStyleVar(); + + ImRect bb(pos, pos + size); + const ImGuiID id = window->GetID(title); + if (enabled) + { + // bit contradictory - we don't want this button to be used for *gamepad* navigation, since they're usually + // activated with the bumpers and/or the back button. + if (!ImGui::ItemAdd(bb, id, nullptr, ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus)) + return false; + } + else + { + if (ImGui::IsClippedEx(bb, id)) + return false; + } + + bool held; + bool pressed; + bool hovered; + if (enabled) + { + pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); + } + else + { + pressed = false; + held = false; + hovered = false; + } + + const ImU32 col = + hovered ? ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f) : + ImGui::GetColorU32(is_active ? background : ImVec4(background.x, background.y, background.z, 0.5f)); + + ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + + if (is_active) + { + const float line_thickness = LayoutScale(2.0f); + ImGui::GetWindowDrawList()->AddLine(ImVec2(bb.Min.x, bb.Max.y - line_thickness), + ImVec2(bb.Max.x, bb.Max.y - line_thickness), + ImGui::GetColorU32(ImGuiCol_TextDisabled), line_thickness); + } + + const ImVec2 pad(std::max((size.x - text_size.x) * 0.5f, 0.0f), std::max((size.y - text_size.y) * 0.5f, 0.0f)); + bb.Min += pad; + bb.Max -= pad; + + ImGui::PushStyleColor( + ImGuiCol_Text, + ImGui::GetColorU32(enabled ? (is_active ? ImGuiCol_Text : ImGuiCol_TextDisabled) : ImGuiCol_ButtonHovered)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); + + ImGui::PopStyleColor(); + + return pressed; +} + void ImGuiFullscreen::PopulateFileSelectorItems() { s_file_selector_items.clear(); @@ -2335,15 +2417,40 @@ void ImGuiFullscreen::DrawBackgroundProgressDialogs(ImVec2& position, float spac // Notifications ////////////////////////////////////////////////////////////////////////// -void ImGuiFullscreen::AddNotification(float duration, std::string title, std::string text, std::string image_path) +void ImGuiFullscreen::AddNotification(std::string key, float duration, std::string title, std::string text, + std::string image_path) { + const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); + + if (!key.empty()) + { + for (auto it = s_notifications.begin(); it != s_notifications.end(); ++it) + { + if (it->key == key) + { + it->duration = duration; + it->title = std::move(title); + it->text = std::move(text); + it->badge_path = std::move(image_path); + + // Don't fade it in again + const float time_passed = + static_cast(Common::Timer::ConvertValueToSeconds(current_time - it->start_time)); + it->start_time = + current_time - Common::Timer::ConvertSecondsToValue(std::min(time_passed, NOTIFICATION_FADE_IN_TIME)); + return; + } + } + } + Notification notif; + notif.key = std::move(key); notif.duration = duration; notif.title = std::move(title); notif.text = std::move(text); notif.badge_path = std::move(image_path); - notif.start_time = Common::Timer::GetCurrentValue(); - notif.move_time = notif.start_time; + notif.start_time = current_time; + notif.move_time = current_time; notif.target_y = -1.0f; notif.last_y = -1.0f; s_notifications.push_back(std::move(notif)); @@ -2359,8 +2466,6 @@ void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing) if (s_notifications.empty()) return; - static constexpr float FADE_IN_TIME = 0.2f; - static constexpr float FADE_OUT_TIME = 0.8f; static constexpr float MOVE_DURATION = 0.5f; const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); @@ -2413,10 +2518,10 @@ void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing) std::max((vertical_padding * 2.0f) + title_size.y + vertical_spacing + text_size.y, min_height); u8 opacity; - if (time_passed < FADE_IN_TIME) - opacity = static_cast((time_passed / FADE_IN_TIME) * 255.0f); - else if (time_passed > (notif.duration - FADE_OUT_TIME)) - opacity = static_cast(std::min((notif.duration - time_passed) / FADE_OUT_TIME, 1.0f) * 255.0f); + if (time_passed < NOTIFICATION_FADE_IN_TIME) + opacity = static_cast((time_passed / NOTIFICATION_FADE_IN_TIME) * 255.0f); + else if (time_passed > (notif.duration - NOTIFICATION_FADE_OUT_TIME)) + opacity = static_cast(std::min((notif.duration - time_passed) / NOTIFICATION_FADE_OUT_TIME, 1.0f) * 255.0f); else opacity = 255; @@ -2431,7 +2536,8 @@ void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing) } else if (actual_y != expected_y) { - const float time_since_move = static_cast(Common::Timer::ConvertValueToSeconds(current_time - notif.move_time)); + const float time_since_move = + static_cast(Common::Timer::ConvertValueToSeconds(current_time - notif.move_time)); if (time_since_move >= MOVE_DURATION) { notif.move_time = current_time; diff --git a/src/util/imgui_fullscreen.h b/src/util/imgui_fullscreen.h index 529d34c78..ad3a01c63 100644 --- a/src/util/imgui_fullscreen.h +++ b/src/util/imgui_fullscreen.h @@ -244,6 +244,8 @@ void RightAlignNavButtons(u32 num_items = 0, float item_width = LAYOUT_MENU_BUTT float item_height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); bool NavButton(const char* title, bool is_active, bool enabled = true, float width = -1.0f, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); +bool NavTab(const char* title, bool is_active, bool enabled, float width, float height, const ImVec4& background, + ImFont* font = g_large_font); using FileSelectorCallback = std::function; using FileSelectorFilters = std::vector; @@ -286,7 +288,7 @@ void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 m void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); void CloseBackgroundProgressDialog(const char* str_id); -void AddNotification(float duration, std::string title, std::string text, std::string image_path); +void AddNotification(std::string key, float duration, std::string title, std::string text, std::string image_path); void ClearNotifications(); void ShowToast(std::string title, std::string message, float duration = 10.0f);