From 255896e89fa4fa71c2550cb8dfab2e4044cfbe48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 5 Sep 2026 13:27:28 -0600 Subject: [PATCH] Re-check hardcore mode when a savestate op is actually applied The check lived only in Enqueue, but operations don't run there - they're queued and applied later by Process(). During boot, HardcoreModeActive() reads false even when hardcore is on, since it requires rc_client_is_processing_required(), which only becomes true once RetroAchievements has finished identifying the game asynchronously. Anything queued in that window passed the check, and was then applied by Process() after identification completed and hardcore came up. Auto-load wasn't even a race: EmuScreen::bootComplete() calls Achievements::SetGame(), which starts the identify, and then checks HardcoreModeActive() a few lines below - always false at that point. So "Auto load savestate" quietly worked in hardcore mode. --state and a load-state hotkey pressed during boot got through the same way. Re-checking per operation in Process() covers every entry point at once, and by then identification has finished, so the answer is authoritative. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup --- Core/SaveState.cpp | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index db983922ca..8f4ba4a853 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -196,17 +196,18 @@ int g_screenshotFailures; pspFileSystem.DoState(p); } - void Enqueue(const SaveState::Operation &op) { - if (!NetworkAllowSaveState()) { - return; + // Hardcore mode bans loading savestates outright, and saving too unless the user opted + // back into that. + bool BannedInHardcoreMode(OperationType type) { + if (!Achievements::HardcoreModeActive()) { + return false; } - if (Achievements::HardcoreModeActive()) { - if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::OperationType::Save))) { - // We allow saving in hardcore mode if this setting is on. - } else { - // Operation not allowed - return; - } + return !(g_Config.bAchievementsSaveStateInHardcoreMode && type == OperationType::Save); + } + + void Enqueue(const SaveState::Operation &op) { + if (!NetworkAllowSaveState() || BannedInHardcoreMode(op.type)) { + return; } std::lock_guard guard(mutex); @@ -810,6 +811,17 @@ int g_screenshotFailures; SaveStart state; for (const auto &op : operations) { + // Re-check here, and not just in Enqueue: during boot, RetroAchievements is still + // identifying the game asynchronously, and until it's done hardcore mode doesn't + // read as active yet. An operation queued in that window (a hotkey press, --state, + // auto-load) passed the check in Enqueue and would otherwise be applied here, after + // identification has finished and hardcore mode has come up. + if (BannedInHardcoreMode(op.type)) { + WARN_LOG(Log::SaveState, "Dropping queued savestate operation - hardcore mode is active"); + Achievements::WarnUserIfHardcoreModeActive(op.type == OperationType::Save); + continue; + } + CChunkFileReader::Error result; Status callbackResult; std::string callbackMessage;