From 1bb1354d4eb7d49715f1fc76fc446aace4d5d68c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 9 Apr 2025 22:26:54 +1000 Subject: [PATCH] Achievements: Use rc_client as source of truth for HC mode --- src/core/achievements.cpp | 444 +++++++++--------- src/core/achievements.h | 29 +- src/core/system.cpp | 48 +- src/duckstation-qt/achievementlogindialog.cpp | 2 +- .../achievementsettingswidget.cpp | 8 +- src/duckstation-qt/mainwindow.cpp | 55 ++- src/duckstation-qt/mainwindow.h | 2 +- src/duckstation-qt/qthost.cpp | 2 +- src/duckstation-qt/qthost.h | 2 +- 9 files changed, 282 insertions(+), 310 deletions(-) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index fb8661a80..685a4ad2a 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -7,6 +7,7 @@ #include "achievements_private.h" #include "bios.h" #include "bus.h" +#include "cheats.h" #include "cpu_core.h" #include "fullscreen_ui.h" #include "game_list.h" @@ -144,14 +145,14 @@ static void ReportRCError(int err, fmt::format_string fmt, T&&... args); static void ClearGameInfo(); static void ClearGameHash(); static bool TryLoggingInWithToken(); -static void SetHardcoreMode(bool enabled, bool force_display_message); +static void EnableHardcodeMode(bool display_message, bool display_game_summary); +static void OnHardcoreModeChanged(bool enabled, bool display_message, bool display_game_summary); static bool IsLoggedInOrLoggingIn(); -static bool CanEnableHardcoreMode(); static void FinishLogin(const rc_client_t* client); static void ShowLoginNotification(); -static void IdentifyGame(const std::string& path, CDImage* image); +static bool IdentifyGame(CDImage* image); +static bool IdentifyCurrentGame(); static void BeginLoadGame(); -static void BeginChangeDisc(); static void UpdateGameSummary(bool update_progress_database, bool force_update_progress_database); static std::string GetLocalImagePath(const std::string_view image_name, int type); static void DownloadImage(std::string url, std::string cache_path); @@ -244,7 +245,6 @@ struct PauseMenuAchievementInfo struct State { rc_client_t* client = nullptr; - bool hardcore_mode = false; bool has_achievements = false; bool has_leaderboards = false; bool has_rich_presence = false; @@ -330,9 +330,8 @@ const rc_client_user_game_summary_t& Achievements::GetGameSummary() void Achievements::ReportError(std::string_view sv) { - std::string error = fmt::format("Achievements error: {}", sv); - ERROR_LOG(error.c_str()); - Host::AddOSDMessage(std::move(error), Host::OSD_CRITICAL_ERROR_DURATION); + ERROR_LOG(sv); + Host::AddIconOSDWarning(std::string(), ICON_EMOJI_WARNING, std::string(sv), Host::OSD_CRITICAL_ERROR_DURATION); } template @@ -581,7 +580,11 @@ bool Achievements::IsHardcoreModeActive() return RA_HardcoreModeIsActive() != 0; #endif - return s_state.hardcore_mode; + if (!s_state.client) + return false; + + const auto lock = GetLock(); + return rc_client_get_hardcore_enabled(s_state.client); } bool Achievements::HasActiveGame() @@ -651,25 +654,27 @@ bool Achievements::Initialize() if (!CreateClient(&s_state.client, &s_state.http_downloader)) return false; - // Hardcore starts off. We enable it on first boot. - s_state.hardcore_mode = false; - rc_client_set_event_handler(s_state.client, ClientEventHandler); - rc_client_set_hardcore_enabled(s_state.client, s_state.hardcore_mode); + // Hardcore starts off. We enable it on first boot. + rc_client_set_hardcore_enabled(s_state.client, false); rc_client_set_encore_mode_enabled(s_state.client, g_settings.achievements_encore_mode); rc_client_set_unofficial_enabled(s_state.client, g_settings.achievements_unofficial_test_mode); rc_client_set_spectator_mode_enabled(s_state.client, g_settings.achievements_spectator_mode); - // Begin disc identification early, before the login finishes. - if (System::IsValid()) - IdentifyGame(System::GetDiscPath(), nullptr); - + // Start logging in. This can take a while. TryLoggingInWithToken(); - // Hardcore mode isn't enabled when achievements first starts, if a game is already running. - if (System::IsValid() && IsLoggedInOrLoggingIn() && g_settings.achievements_hardcore_mode) - DisplayHardcoreDeferredMessage(); + // Are we running a game? + if (System::IsValid()) + { + IdentifyCurrentGame(); + BeginLoadGame(); + + // Hardcore mode isn't enabled when achievements first starts, if a game is already running. + if (IsLoggedInOrLoggingIn() && g_settings.achievements_hardcore_mode) + DisplayHardcoreDeferredMessage(); + } return true; } @@ -753,7 +758,7 @@ void Achievements::UpdateSettings(const Settings& old_config) if (!g_settings.achievements_enabled) { // we're done here - Shutdown(false); + Shutdown(); return; } @@ -766,26 +771,23 @@ void Achievements::UpdateSettings(const Settings& old_config) if (g_settings.achievements_hardcore_mode != old_config.achievements_hardcore_mode) { - // Hardcore mode can only be enabled through reset (ResetChallengeMode()). - if (s_state.hardcore_mode && !g_settings.achievements_hardcore_mode) - { - ResetHardcoreMode(false); - } - else if (!s_state.hardcore_mode && g_settings.achievements_hardcore_mode) - { - if (HasActiveGame()) - DisplayHardcoreDeferredMessage(); - } + // Enables have to wait for reset, disables can go through immediately. + if (g_settings.achievements_hardcore_mode) + DisplayHardcoreDeferredMessage(); + else + DisableHardcoreMode(true, true); } // These cannot be modified while a game is loaded, so just toss state and reload. + auto lock = GetLock(); if (HasActiveGame()) { + lock.unlock(); if (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) { - Shutdown(false); + Shutdown(); Initialize(); return; } @@ -801,48 +803,27 @@ void Achievements::UpdateSettings(const Settings& old_config) } } -bool Achievements::Shutdown(bool allow_cancel) +void Achievements::Shutdown() { -#ifdef ENABLE_RAINTEGRATION - if (IsUsingRAIntegration()) - { - if (System::IsValid() && allow_cancel && !RA_ConfirmLoadNewRom(true)) - return false; - - RA_SetPaused(false); - RA_ActivateGame(0); - return true; - } -#endif - if (!IsActive()) - return true; + return; auto lock = GetLock(); Assert(s_state.client && s_state.http_downloader); ClearGameInfo(); ClearGameHash(); - DisableHardcoreMode(); + DisableHardcoreMode(false, false); UpdateGlyphRanges(); CancelHashDatabaseRequests(); - if (s_state.load_game_request) - { - rc_client_abort_async(s_state.client, s_state.load_game_request); - s_state.load_game_request = nullptr; - } if (s_state.login_request) { rc_client_abort_async(s_state.client, s_state.login_request); s_state.login_request = nullptr; } - s_state.hardcore_mode = false; DestroyClient(&s_state.client, &s_state.http_downloader); - - Host::OnAchievementsRefreshed(); - return true; } void Achievements::ClientMessageCallback(const char* message, const rc_client_t* client) @@ -892,7 +873,7 @@ void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_s RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) : status_code; rr.body_length = data.size(); - rr.body = reinterpret_cast(data.data()); + rr.body = data.empty() ? nullptr : reinterpret_cast(data.data()); callback(&rr, callback_data); }; @@ -1147,40 +1128,109 @@ void Achievements::UpdateRichPresence(std::unique_lock& lo lock.lock(); } -void Achievements::GameChanged(const std::string& path, CDImage* image, bool booting) +void Achievements::OnSystemStarting(CDImage* image, bool disable_hardcore_mode) { std::unique_lock lock(s_state.mutex); if (!IsActive()) return; - IdentifyGame(path, image); - // if we're not logged in, and there's no login request, retry logging in // this'll happen if we had no network connection on startup, but gained it before starting a game. - // follow the same order as Initialize() - identify, then log in - if (!IsLoggedInOrLoggingIn() && booting) + if (!IsLoggedInOrLoggingIn()) { WARNING_LOG("Not logged in on game booting, trying again."); TryLoggingInWithToken(); } + + // HC should have been disabled, we're now enabling it + // only enable hardcore mode if we're logged in, or waiting for a login response + AssertMsg(!rc_client_get_hardcore_enabled(s_state.client), "Hardcode mode should be disabled prior to boot"); + if (!disable_hardcore_mode && g_settings.achievements_hardcore_mode && IsLoggedInOrLoggingIn()) + EnableHardcodeMode(false, false); + + // now we can finally identify the game + IdentifyGame(image); + BeginLoadGame(); } -void Achievements::IdentifyGame(const std::string& path, CDImage* image) +void Achievements::OnSystemDestroyed() { - if (s_state.game_path == path) + ClearGameInfo(); + ClearGameHash(); + DisableHardcoreMode(false, false); + UpdateGlyphRanges(); +} + +void Achievements::OnSystemPaused(bool paused) +{ +#ifdef ENABLE_RAINTEGRATION + if (IsUsingRAIntegration()) + RA_SetPaused(paused); +#endif +} + +void Achievements::OnSystemReset() +{ + const auto lock = GetLock(); + if (!IsActive()) + return; + +#ifdef ENABLE_RAINTEGRATION + if (IsUsingRAIntegration()) { - WARNING_LOG("Game path is unchanged."); + RA_OnReset(); return; } +#endif - std::unique_ptr temp_image; - if (!path.empty() && !image) + // Do we need to enable hardcore mode? + if (System::IsValid() && g_settings.achievements_hardcore_mode && !rc_client_get_hardcore_enabled(s_state.client)) { - temp_image = CDImage::Open(path.c_str(), g_settings.cdrom_load_image_patches, nullptr); - image = temp_image.get(); - if (!temp_image) - ERROR_LOG("Failed to open temporary CD image '{}'", path); + // This will raise the silly reset event, but we can safely ignore that since we're immediately resetting the client + DEV_LOG("Enabling hardcore mode after reset"); + EnableHardcodeMode(true, true); + } + + DEV_LOG("Reset client"); + rc_client_reset(s_state.client); +} + +void Achievements::GameChanged(CDImage* image) +{ + std::unique_lock lock(s_state.mutex); + + if (!IsActive()) + return; + + // disc changed? + if (!IdentifyGame(image)) + return; + + // cancel previous requests + if (s_state.load_game_request) + { + rc_client_abort_async(s_state.client, s_state.load_game_request); + s_state.load_game_request = nullptr; + } + + // Use a hash that will never match if we removed the disc. See rc_client_begin_change_media(). + TinyString game_hash_str; + if (s_state.game_hash.has_value()) + game_hash_str = GameHashToString(s_state.game_hash.value()); + else + game_hash_str = "[NO HASH]"; + + s_state.load_game_request = rc_client_begin_change_media_from_hash( + s_state.client, game_hash_str.c_str(), ClientLoadGameCallback, reinterpret_cast(static_cast(1))); +} + +bool Achievements::IdentifyGame(CDImage* image) +{ + if (s_state.game_path == image->GetPath()) + { + WARNING_LOG("Game path is unchanged."); + return false; } std::optional game_hash; @@ -1189,63 +1239,64 @@ void Achievements::IdentifyGame(const std::string& path, CDImage* image) u32 bytes_hashed; game_hash = GetGameHash(image, &bytes_hashed); if (game_hash.has_value()) - INFO_LOG("RA Hash: {} ({} bytes hashed)", GameHashToString(game_hash.value()), bytes_hashed); + { + INFO_COLOR_LOG(StrongOrange, "RA Hash: {} ({} bytes hashed)", GameHashToString(game_hash.value()), bytes_hashed); + } + else + { + // If we are starting with this game and it's bad, notify the user that this is why. + Host::AddIconOSDWarning( + "AchievementsHashFailed", ICON_EMOJI_WARNING, + TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."), + Host::OSD_ERROR_DURATION); + } } + s_state.game_path = image ? image->GetPath() : std::string(); + if (s_state.game_hash == game_hash) { // only the path has changed - different format/save state/etc. - INFO_LOG("Detected path change from '{}' to '{}'", s_state.game_path, path); - s_state.game_path = path; - return; + INFO_LOG("Detected path change to '{}'", s_state.game_path); + s_state.game_path = image->GetPath(); + return false; } - ClearGameHash(); - s_state.game_path = path; - s_state.game_hash = std::move(game_hash); + s_state.game_hash = game_hash; #ifdef ENABLE_RAINTEGRATION if (IsUsingRAIntegration()) - { RAIntegration::GameChanged(); - return; - } #endif - // shouldn't have a load game request when we're not logged in. - Assert(IsLoggedInOrLoggingIn() || !s_state.load_game_request); + return true; +} - // bail out if we're not logged in, just save the hash - if (!IsLoggedInOrLoggingIn()) +bool Achievements::IdentifyCurrentGame() +{ + DebugAssert(System::IsValid()); + + // this crap is only needed because we can't grab the image from the reader... + std::unique_ptr temp_image; + if (const std::string& disc_path = System::GetDiscPath(); !disc_path.empty()) { - INFO_LOG("Skipping load game because we're not logged in."); - DisableHardcoreMode(); - return; + Error error; + temp_image = CDImage::Open(disc_path.c_str(), g_settings.cdrom_load_image_patches, &error); + if (!temp_image) + ERROR_LOG("Failed to open disc for late game identification: {}", error.GetDescription()); } - if (!rc_client_is_game_loaded(s_state.client)) - BeginLoadGame(); - else - BeginChangeDisc(); + return IdentifyGame(temp_image.get()); } void Achievements::BeginLoadGame() { - ClearGameInfo(); + DebugAssert(IsLoggedInOrLoggingIn()); if (!s_state.game_hash.has_value()) { - // when we're booting the bios, this will fail - if (!s_state.game_path.empty()) - { - Host::AddKeyedOSDMessage( - "retroachievements_disc_read_failed", - TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."), - Host::OSD_ERROR_DURATION); - } - - DisableHardcoreMode(); - UpdateGlyphRanges(); + // no need to go through ClientLoadGameCallback, just bail out straight away + DisableHardcoreMode(false, false); return; } @@ -1253,37 +1304,6 @@ void Achievements::BeginLoadGame() ClientLoadGameCallback, nullptr); } -void Achievements::BeginChangeDisc() -{ - // cancel previous requests - if (s_state.load_game_request) - { - rc_client_abort_async(s_state.client, s_state.load_game_request); - s_state.load_game_request = nullptr; - } - - if (!s_state.game_hash.has_value()) - { - // when we're booting the bios, this will fail - if (!s_state.game_path.empty()) - { - Host::AddKeyedOSDMessage( - "retroachievements_disc_read_failed", - TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."), - Host::OSD_ERROR_DURATION); - } - - ClearGameInfo(); - DisableHardcoreMode(); - UpdateGlyphRanges(); - return; - } - - s_state.load_game_request = - rc_client_begin_change_media_from_hash(s_state.client, GameHashToString(s_state.game_hash.value()), - ClientLoadGameCallback, reinterpret_cast(static_cast(1))); -} - void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) { const bool was_disc_change = (userdata != nullptr); @@ -1300,7 +1320,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, UpdateGlyphRanges(); } - DisableHardcoreMode(); + DisableHardcoreMode(false, false); return; } else if (result == RC_LOGIN_REQUIRED) @@ -1309,6 +1329,14 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, // Once we've done so, we'll reload the game. return; } + else if (result == RC_HARDCORE_DISABLED) + { + if (error_message) + ReportError(error_message); + + OnHardcoreModeChanged(false, true, false); + return; + } else if (result != RC_OK) { ReportFmtError("Loading game failed: {}", error_message); @@ -1318,16 +1346,9 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, UpdateGlyphRanges(); } - DisableHardcoreMode(); + DisableHardcoreMode(false, false); return; } - else if (result == RC_HARDCORE_DISABLED) - { - if (error_message) - ReportError(error_message); - - DisableHardcoreMode(); - } const rc_client_game_t* info = rc_client_get_game_info(s_state.client); if (!info) @@ -1339,7 +1360,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, UpdateGlyphRanges(); } - DisableHardcoreMode(); + DisableHardcoreMode(false, false); return; } @@ -1349,13 +1370,10 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, // Only display summary if the game title has changed across discs. const bool display_summary = (s_state.game_id != info->id || s_state.game_title != info->title); - // If the game has a RetroAchievements entry but no achievements or leaderboards, - // enforcing hardcore mode is pointless. - if (!has_achievements && !has_leaderboards) - DisableHardcoreMode(); - - // We should have matched hardcore mode state. - Assert(s_state.hardcore_mode == (rc_client_get_hardcore_enabled(client) != 0)); + // If the game has a RetroAchievements entry but no achievements or leaderboards, enforcing hardcore mode + // is pointless. Have to re-query leaderboards because hidden should still trip HC. + if (!has_achievements && !rc_client_has_leaderboards(client, true)) + DisableHardcoreMode(false, false); s_state.game_id = info->id; s_state.game_title = info->title; @@ -1469,7 +1487,7 @@ void Achievements::DisplayAchievementSummary() void Achievements::DisplayHardcoreDeferredMessage() { - if (g_settings.achievements_hardcore_mode && !s_state.hardcore_mode && System::IsValid()) + if (g_settings.achievements_hardcore_mode && System::IsValid()) { GPUThread::RunOnThread([]() { if (!FullscreenUI::Initialize()) @@ -1484,12 +1502,7 @@ void Achievements::DisplayHardcoreDeferredMessage() void Achievements::HandleResetEvent(const rc_client_event_t* event) { - // We handle system resets ourselves, but still need to reset the client's state. - INFO_LOG("Resetting runtime due to reset event"); - rc_client_reset(s_state.client); - - if (HasActiveGame()) - UpdateGameSummary(false, false); + WARNING_LOG("Ignoring RC_CLIENT_EVENT_RESET."); } void Achievements::HandleUnlockEvent(const rc_client_event_t* event) @@ -1806,32 +1819,17 @@ void Achievements::HandleServerReconnectedEvent(const rc_client_event_t* event) }); } -void Achievements::Reset() +void Achievements::EnableHardcodeMode(bool display_message, bool display_game_summary) { -#ifdef ENABLE_RAINTEGRATION - if (IsUsingRAIntegration()) - { - RA_OnReset(); - return; - } -#endif - - if (!IsActive()) + DebugAssert(IsActive()); + if (rc_client_get_hardcore_enabled(s_state.client)) return; - DEV_LOG("Reset client"); - rc_client_reset(s_state.client); + rc_client_set_hardcore_enabled(s_state.client, true); + OnHardcoreModeChanged(true, display_message, display_game_summary); } -void Achievements::OnSystemPaused(bool paused) -{ -#ifdef ENABLE_RAINTEGRATION - if (IsUsingRAIntegration()) - RA_SetPaused(paused); -#endif -} - -void Achievements::DisableHardcoreMode() +void Achievements::DisableHardcoreMode(bool show_message, bool display_game_summary) { if (!IsActive()) return; @@ -1846,43 +1844,19 @@ void Achievements::DisableHardcoreMode() } #endif - if (!s_state.hardcore_mode) - return; - - SetHardcoreMode(false, true); -} - -bool Achievements::ResetHardcoreMode(bool is_booting) -{ - if (!IsActive()) - return false; - 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 = (IsLoggedInOrLoggingIn() || s_state.load_game_request) && - (HasActiveGame() || s_state.load_game_request) && - g_settings.achievements_hardcore_mode; - if (s_state.hardcore_mode == wanted_hardcore_mode) - return false; - - if (!is_booting && wanted_hardcore_mode && !CanEnableHardcoreMode()) - return false; - - SetHardcoreMode(wanted_hardcore_mode, false); - return true; -} - -void Achievements::SetHardcoreMode(bool enabled, bool force_display_message) -{ - if (enabled == s_state.hardcore_mode) + if (!rc_client_get_hardcore_enabled(s_state.client)) return; - // new mode - s_state.hardcore_mode = enabled; + rc_client_set_hardcore_enabled(s_state.client, false); + OnHardcoreModeChanged(false, show_message, display_game_summary); +} - if (System::IsValid() && (HasActiveGame() || force_display_message)) +void Achievements::OnHardcoreModeChanged(bool enabled, bool display_message, bool display_game_summary) +{ + INFO_COLOR_LOG(StrongYellow, "Hardcore mode/restrictions are now {}.", enabled ? "ACTIVE" : "inactive"); + + if (System::IsValid() && display_message) { GPUThread::RunOnThread([enabled]() { if (!FullscreenUI::Initialize()) @@ -1895,17 +1869,28 @@ void Achievements::SetHardcoreMode(bool enabled, bool force_display_message) }); } - rc_client_set_hardcore_enabled(s_state.client, enabled); - DebugAssert((rc_client_get_hardcore_enabled(s_state.client) != 0) == enabled); - if (HasActiveGame()) + if (HasActiveGame() && display_game_summary) { UpdateGameSummary(true, true); DisplayAchievementSummary(); } + DebugAssert((rc_client_get_hardcore_enabled(s_state.client) != 0) == enabled); + // Reload setting to permit cheating-like things if we were just disabled. - if (!enabled && System::IsValid()) + if (System::IsValid()) + { + // Make sure a pre-existing cheat file hasn't been loaded when resetting after enabling HC mode. + Cheats::ReloadCheats(true, true, false, true, true); + + // Defer settings update in case something is using it. Host::RunOnCPUThread([]() { System::ApplySettings(false); }); + } + else if (System::GetState() == System::State::Starting) + { + // Initial HC enable, activate restrictions. + System::ApplySettings(false); + } // Toss away UI state, because it's invalid now ClearUIState(); @@ -1947,9 +1932,6 @@ bool Achievements::DoState(StateWrapper& sw) GPUThread::RunOnThread([]() { FullscreenUI::CloseLoadingScreen(); }); } - // loading an old state without cheevos, so reset the runtime - Achievements::Reset(); - u32 data_size = 0; sw.DoEx(&data_size, REQUIRED_VERSION, 0u); if (data_size == 0) @@ -2096,12 +2078,6 @@ bool Achievements::IsLoggedInOrLoggingIn() return (rc_client_get_user_info(s_state.client) != nullptr || s_state.login_request); } -bool Achievements::CanEnableHardcoreMode() -{ - // have to re-query leaderboards because hidden should still trip HC - return (s_state.load_game_request || s_state.has_achievements || rc_client_has_leaderboards(s_state.client, true)); -} - bool Achievements::Login(const char* username, const char* password, Error* error) { auto lock = GetLock(); @@ -2145,7 +2121,10 @@ bool Achievements::Login(const char* username, const char* password, Error* erro // If we were't a temporary client, get the game loaded. if (System::IsValid() && !is_temporary_client) + { + IdentifyCurrentGame(); BeginLoadGame(); + } return true; } @@ -2223,9 +2202,6 @@ void Achievements::ClientLoginWithTokenCallback(int result, const char* error_me } FinishLogin(client); - - if (System::IsValid()) - BeginLoadGame(); } void Achievements::FinishLogin(const rc_client_t* client) @@ -2338,7 +2314,7 @@ void Achievements::Logout() ClearProgressDatabase(); } -bool Achievements::ConfirmSystemReset() +bool Achievements::ConfirmGameChange() { #ifdef ENABLE_RAINTEGRATION if (IsUsingRAIntegration()) @@ -2364,7 +2340,7 @@ bool Achievements::ConfirmHardcoreModeDisable(const char* trigger) if (!confirmed) return false; - DisableHardcoreMode(); + DisableHardcoreMode(true, true); return true; } @@ -2374,7 +2350,7 @@ void Achievements::ConfirmHardcoreModeDisableAsync(const char* trigger, std::fun // don't run the callback in the middle of rendering the UI Host::RunOnCPUThread([callback = std::move(callback), res]() { if (res) - DisableHardcoreMode(); + DisableHardcoreMode(true, true); callback(res); }); }; @@ -2826,7 +2802,7 @@ void Achievements::DrawAchievementsWindow() using ImGuiFullscreen::RenderShadowedTextClipped; using ImGuiFullscreen::UIStyle; - auto lock = Achievements::GetLock(); + const auto lock = Achievements::GetLock(); // achievements can get turned off via the main UI if (!s_state.achievement_list) @@ -2886,7 +2862,7 @@ void Achievements::DrawAchievementsWindow() const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFont->FontSize)); text.assign(s_state.game_title); - if (s_state.hardcore_mode) + if (rc_client_get_hardcore_enabled(s_state.client)) text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)")); top += UIStyle.LargeFont->FontSize + spacing; diff --git a/src/core/achievements.h b/src/core/achievements.h index 72e509310..ca6a7c14d 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -77,18 +77,24 @@ bool Initialize(); /// Updates achievements settings. void UpdateSettings(const Settings& old_config); -/// Resets the internal state of all achievement tracking. Call on system reset. -void Reset(); +/// Shuts down the RetroAchievements client. +void Shutdown(); -/// Called when the system is being reset. If it returns false, the reset should be aborted. -bool ConfirmSystemReset(); +/// Called when the system is start. Engages hardcore mode if enabled. +void OnSystemStarting(CDImage* image, bool disable_hardcore_mode); -/// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted. -bool Shutdown(bool allow_cancel); +/// Called when the system is shutting down. If this returns false, the shutdown should be aborted. +void OnSystemDestroyed(); + +/// Called when the system is being reset. Resets the internal state of all achievement tracking. +void OnSystemReset(); /// Called when the system is being paused and resumed. void OnSystemPaused(bool paused); +/// Called when the system changes game. +void GameChanged(CDImage* image); + /// Called once a frame at vsync time on the CPU thread. void FrameUpdate(); @@ -108,14 +114,8 @@ bool Login(const char* username, const char* password, Error* error); /// 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, bool booting); - -/// Re-enables hardcore mode if it is enabled in the settings. -bool ResetHardcoreMode(bool is_booting); - /// Forces hardcore mode off until next reset. -void DisableHardcoreMode(); +void DisableHardcoreMode(bool show_message, bool display_game_summary); /// Prompts the user to disable hardcore mode, if they agree, returns true. bool ConfirmHardcoreModeDisable(const char* trigger); @@ -127,6 +127,9 @@ bool IsHardcoreModeActive(); /// RAIntegration only exists for Windows, so no point checking it on other platforms. bool IsUsingRAIntegration(); +/// Hook for RAIntegration to confirm reset/shutdown. +bool ConfirmGameChange(); + /// Returns true if the achievement system is active. Achievements can be active without a valid client. bool IsActive(); diff --git a/src/core/system.cpp b/src/core/system.cpp index 1dac1c651..30ae7f61a 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -542,7 +542,7 @@ void System::CPUThreadShutdown() ShutdownDiscordPresence(); #endif - Achievements::Shutdown(false); + Achievements::Shutdown(); InputManager::CloseSources(); @@ -1349,12 +1349,17 @@ void System::ApplySettings(bool display_osd_messages) LoadSettings(display_osd_messages); // If we've disabled/enabled game settings, we need to reload without it. - // Also reload cheats when safe mode is toggled, because patches might change. if (g_settings.apply_game_settings != old_settings.apply_game_settings) { UpdateGameSettingsLayer(); LoadSettings(display_osd_messages); } + else if (g_settings.achievements_hardcore_mode != old_settings.achievements_hardcore_mode) + { + // Hardcore mode enabled/disabled. May need to disable restrictions. + Achievements::UpdateSettings(old_settings); + LoadSettings(display_osd_messages); + } CheckForSettingsChanges(old_settings); Host::CheckForSettingsChanges(old_settings); @@ -1546,19 +1551,9 @@ void System::UpdateInputSettingsLayer(std::string input_profile_name, std::uniqu void System::ResetSystem() { - if (!IsValid()) + if (!IsValid() || !Achievements::ConfirmGameChange()) return; - if (!Achievements::ConfirmSystemReset()) - return; - - if (Achievements::ResetHardcoreMode(false)) - { - // Make sure a pre-existing cheat file hasn't been loaded when resetting after enabling HC mode. - Cheats::ReloadCheats(true, true, false, true, true); - ApplySettings(false); - } - InternalReset(); // Reset boot mode/reload BIOS if needed. Preserve exe/psf boot. @@ -1731,6 +1726,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) // Update running game, this will apply settings as well. UpdateRunningGame(disc ? disc->GetPath() : parameters.filename, disc.get(), true); + Achievements::OnSystemStarting(disc.get(), parameters.disable_achievements_hardcore_mode); // Determine console region. Has to be done here, because gamesettings can override it. s_state.region = (g_settings.region == ConsoleRegion::Auto) ? auto_console_region : g_settings.region; @@ -1755,13 +1751,6 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) // Achievement hardcore checks before committing to anything. if (disc) { - // Check for resuming with hardcore mode. - const bool hc_mode_was_enabled = Achievements::IsHardcoreModeActive(); - if (parameters.disable_achievements_hardcore_mode) - Achievements::DisableHardcoreMode(); - else - Achievements::ResetHardcoreMode(true); - if ((!parameters.save_state.empty() || !exe_override.empty()) && Achievements::IsHardcoreModeActive()) { const bool is_exe_override_boot = parameters.save_state.empty(); @@ -1794,10 +1783,6 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) return true; } } - - // Need to reinit things like emulation speed, cpu overclock, etc. - if (Achievements::IsHardcoreModeActive() != hc_mode_was_enabled) - ApplySettings(false); } // Are we fast booting? Must be checked after updating game settings. @@ -1994,6 +1979,7 @@ void System::DestroySystem() CPU::Shutdown(); Bus::Shutdown(); TimingEvents::Shutdown(); + Achievements::OnSystemDestroyed(); ClearRunningGame(); GPUThread::DestroyGPUBackend(); @@ -2043,8 +2029,6 @@ void System::ClearRunningGame() Host::OnGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, s_state.running_game_hash); - Achievements::GameChanged(s_state.running_game_path, nullptr, false); - UpdateRichPresence(true); } @@ -2780,7 +2764,7 @@ void System::InternalReset() MDEC::Reset(); SIO::Reset(); PCDrv::Reset(); - Achievements::Reset(); + Achievements::OnSystemReset(); s_state.frame_number = 1; s_state.internal_frame_number = 0; } @@ -2963,7 +2947,7 @@ bool System::LoadStateFromBuffer(const SaveStateBuffer& buffer, Error* error, bo ClearMemorySaveStates(false, false); // Updating game/loading settings can turn on hardcore mode. Catch this. - Achievements::DisableHardcoreMode(); + Achievements::DisableHardcoreMode(true, true); return LoadStateDataFromBuffer(buffer.state_data.cspan(0, buffer.state_size), buffer.version, error, update_display); } @@ -4205,18 +4189,18 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo } UpdateGameSettingsLayer(); + ApplySettings(true); if (!IsReplayingGPUDump()) { - Achievements::GameChanged(s_state.running_game_path, image, booting); - // Cheats are loaded later in Initialize(). if (!booting) + { + Achievements::GameChanged(image); Cheats::ReloadCheats(true, true, false, true, true); + } } - ApplySettings(true); - if (s_state.running_game_serial != prev_serial) { GPUThread::SetGameSerial(s_state.running_game_serial); diff --git a/src/duckstation-qt/achievementlogindialog.cpp b/src/duckstation-qt/achievementlogindialog.cpp index e5bd46a6a..508d3133e 100644 --- a/src/duckstation-qt/achievementlogindialog.cpp +++ b/src/duckstation-qt/achievementlogindialog.cpp @@ -56,7 +56,7 @@ void AchievementLoginDialog::cancelClicked() { Host::RunOnCPUThread([]() { if (System::IsValid() && !Achievements::HasActiveGame()) - Achievements::DisableHardcoreMode(); + Achievements::DisableHardcoreMode(false, false); }); } diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp index 6b7e09a00..c0897d135 100644 --- a/src/duckstation-qt/achievementsettingswidget.cpp +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -138,9 +138,11 @@ void AchievementSettingsWidget::onHardcoreModeStateChanged() return; // don't bother prompting if the game doesn't have achievements - auto lock = Achievements::GetLock(); - if (!Achievements::HasActiveGame()) - return; + { + auto lock = Achievements::GetLock(); + if (!Achievements::HasActiveGame()) + return; + } if (QMessageBox::question( QtUtils::GetRootWidget(this), tr("Reset System"), diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 35ebc5a52..6120feb39 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -104,8 +104,10 @@ MainWindow* g_main_window = nullptr; // UI thread VM validity. static bool s_disable_window_rounded_corners = false; +static bool s_system_starting = false; static bool s_system_valid = false; static bool s_system_paused = false; +static bool s_achievements_hardcore_mode = false; static bool s_fullscreen_ui_started = false; static std::atomic_uint32_t s_system_locked{false}; static QString s_current_game_title; @@ -170,6 +172,7 @@ void MainWindow::initialize() m_ui.setupUi(this); setupAdditionalUi(); updateToolbarActions(); + updateEmulationActions(false, false, false); connectSignals(); restoreStateFromConfig(); @@ -515,18 +518,20 @@ void MainWindow::onMouseModeRequested(bool relative_mode, bool hide_cursor) void MainWindow::onSystemStarting() { + s_system_starting = true; s_system_valid = false; s_system_paused = false; switchToEmulationView(); - updateEmulationActions(true, false, Achievements::IsHardcoreModeActive()); + updateEmulationActions(true, false, s_achievements_hardcore_mode); } void MainWindow::onSystemStarted() { m_was_disc_change_request = false; + s_system_starting = false; s_system_valid = true; - updateEmulationActions(false, true, Achievements::IsHardcoreModeActive()); + updateEmulationActions(false, true, s_achievements_hardcore_mode); updateWindowTitle(); updateStatusBarWidgetVisibility(); updateDisplayWidgetCursor(); @@ -574,6 +579,7 @@ void MainWindow::onSystemDestroyed() m_ui.actionPause->setChecked(false); } + s_system_starting = false; s_system_valid = false; s_system_paused = false; @@ -585,7 +591,7 @@ void MainWindow::onSystemDestroyed() return; } - updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); + updateEmulationActions(false, false, s_achievements_hardcore_mode); if (m_display_widget) updateDisplayWidgetCursor(); else @@ -770,7 +776,7 @@ void MainWindow::recreate() if (was_display_created) { g_emu_thread->setSurfaceless(false); - g_main_window->updateEmulationActions(false, System::IsValid(), Achievements::IsHardcoreModeActive()); + g_main_window->updateEmulationActions(false, s_system_valid, s_achievements_hardcore_mode); g_main_window->onFullscreenUIStartedOrStopped(s_fullscreen_ui_started); } @@ -821,7 +827,6 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg { std::vector available_states(System::GetAvailableSaveStates(entry->serial)); const QString timestamp_format = QLocale::system().dateTimeFormat(QLocale::ShortFormat); - const bool challenge_mode = Achievements::IsHardcoreModeActive(); for (SaveStateInfo& ssi : available_states) { if (ssi.global) @@ -835,7 +840,6 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg if (slot < 0) { resume_action->setText(tr("Resume (%1)").arg(timestamp_str)); - resume_action->setEnabled(!challenge_mode); action = resume_action; } else @@ -844,7 +848,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg action = load_state_menu->addAction(tr("Game Save %1 (%2)").arg(slot).arg(timestamp_str)); } - action->setDisabled(challenge_mode); + action->setDisabled(s_achievements_hardcore_mode); connect(action, &QAction::triggered, [this, entry, path = std::move(ssi.path)]() { startFile(entry->path, std::move(path), std::nullopt); }); } @@ -1494,13 +1498,14 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) g_emu_thread->bootSystem(std::move(boot_params)); }); - if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive()) + if (m_ui.menuDebug->menuAction()->isVisible()) { connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() { openCPUDebugger(); std::shared_ptr boot_params = getSystemBootParameters(entry->path); boot_params->override_start_paused = true; + boot_params->disable_achievements_hardcore_mode = true; g_emu_thread->bootSystem(std::move(boot_params)); }); } @@ -1805,14 +1810,14 @@ void MainWindow::onToolbarContextMenuRequested(const QPoint& pos) updateToolbarActions(); } -void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) +void MainWindow::updateEmulationActions(bool starting, bool running, bool achievements_hardcore_mode) { const bool starting_or_running = (starting || running); const bool starting_or_not_running = (starting || !running); m_ui.actionStartFile->setDisabled(starting_or_running); m_ui.actionStartDisc->setDisabled(starting_or_running); m_ui.actionStartBios->setDisabled(starting_or_running); - m_ui.actionResumeLastState->setDisabled(starting_or_running || cheevos_challenge_mode); + m_ui.actionResumeLastState->setDisabled(starting_or_running || achievements_hardcore_mode); m_ui.actionStartFullscreenUI->setDisabled(starting_or_running); m_ui.actionStartFullscreenUI2->setDisabled(starting_or_running); @@ -1821,16 +1826,16 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo m_ui.actionReset->setDisabled(starting_or_not_running); m_ui.actionPause->setDisabled(starting_or_not_running); m_ui.actionChangeDisc->setDisabled(starting_or_not_running); - m_ui.actionCheatsToolbar->setDisabled(starting_or_not_running || cheevos_challenge_mode); + m_ui.actionCheatsToolbar->setDisabled(starting_or_not_running || achievements_hardcore_mode); m_ui.actionScreenshot->setDisabled(starting_or_not_running); m_ui.menuChangeDisc->setDisabled(starting_or_not_running); - m_ui.menuCheats->setDisabled(starting_or_not_running || cheevos_challenge_mode); - m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode); - m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode); + m_ui.menuCheats->setDisabled(starting_or_not_running || achievements_hardcore_mode); + m_ui.actionCPUDebugger->setDisabled(achievements_hardcore_mode); + m_ui.actionMemoryScanner->setDisabled(achievements_hardcore_mode); m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running); - m_ui.actionDumpRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); - m_ui.actionDumpVRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); - m_ui.actionDumpSPURAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); + m_ui.actionDumpRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); + m_ui.actionDumpVRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); + m_ui.actionDumpSPURAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); m_ui.actionSaveState->setDisabled(starting_or_not_running); m_ui.menuSaveState->setDisabled(starting_or_not_running); @@ -2045,8 +2050,6 @@ void MainWindow::switchToEmulationView() void MainWindow::connectSignals() { - updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); - connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); connect(m_ui.toolBar, &QToolBar::customContextMenuRequested, this, &MainWindow::onToolbarContextMenuRequested); @@ -2165,8 +2168,8 @@ void MainWindow::connectSignals() connect(g_emu_thread, &EmuThread::fullscreenUIStartedOrStopped, this, &MainWindow::onFullscreenUIStartedOrStopped); connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); connect(g_emu_thread, &EmuThread::achievementsLoginSuccess, this, &MainWindow::onAchievementsLoginSuccess); - connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this, - &MainWindow::onAchievementsChallengeModeChanged); + connect(g_emu_thread, &EmuThread::achievementsHardcoreModeChanged, this, + &MainWindow::onAchievementsHardcoreModeChanged); connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(g_emu_thread, &EmuThread::onCreateAuxiliaryRenderWindow, this, &MainWindow::onCreateAuxiliaryRenderWindow, Qt::BlockingQueuedConnection); @@ -2795,7 +2798,7 @@ void MainWindow::onAchievementsLoginSuccess(const QString& username, quint32 poi } } -void MainWindow::onAchievementsChallengeModeChanged(bool enabled) +void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) { if (enabled) { @@ -2803,7 +2806,8 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled) QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); } - updateEmulationActions(false, System::IsValid(), enabled); + s_achievements_hardcore_mode = enabled; + updateEmulationActions(s_system_starting, s_system_valid, enabled); } bool MainWindow::onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, qint32 y, quint32 width, quint32 height, @@ -2889,7 +2893,7 @@ void MainWindow::onToolsMediaCaptureToggled(bool checked) void MainWindow::onToolsMemoryScannerTriggered() { - if (Achievements::IsHardcoreModeActive()) + if (s_achievements_hardcore_mode) return; if (!m_memory_scanner_window) @@ -2913,6 +2917,9 @@ void MainWindow::onToolsISOBrowserTriggered() void MainWindow::openCPUDebugger() { + if (s_achievements_hardcore_mode) + return; + if (!m_debugger_window) { m_debugger_window = new DebuggerWindow(); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 917d16255..c4c1048fe 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -151,7 +151,7 @@ private Q_SLOTS: void onMediaCaptureStopped(); void onAchievementsLoginRequested(Achievements::LoginRequestReason reason); void onAchievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages); - void onAchievementsChallengeModeChanged(bool enabled); + void onAchievementsHardcoreModeChanged(bool enabled); bool onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index c2ee72234..d314615b5 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1677,7 +1677,7 @@ void Host::OnAchievementsRefreshed() void Host::OnAchievementsHardcoreModeChanged(bool enabled) { - emit g_emu_thread->achievementsChallengeModeChanged(enabled); + emit g_emu_thread->achievementsHardcoreModeChanged(enabled); } void Host::OnCoverDownloaderOpenRequested() diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index a4ba8ecec..9c343a887 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -155,7 +155,7 @@ Q_SIGNALS: void achievementsLoginRequested(Achievements::LoginRequestReason reason); void achievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages); void achievementsRefreshed(quint32 id, const QString& game_info_string); - void achievementsChallengeModeChanged(bool enabled); + void achievementsHardcoreModeChanged(bool enabled); void cheatEnabled(quint32 index, bool enabled); void mediaCaptureStarted(); void mediaCaptureStopped();