From 681a60b3ca7880c257d4650c95478fd9fed28e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 19 Oct 2026 10:07:30 +0200 Subject: [PATCH] Debugger: Fail game.state.load/save when the savestate is refused SaveState::Load and Save silently drop the operation when the network or achievements hardcore mode forbids it, but the debugger still answered as if it had been queued. Return whether it was, and check the boot state in the same CPU-thread call. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Debugger/WebSocket/GameSubscriber.cpp | 40 ++++++++++++++-------- Core/SaveState.cpp | 19 +++++----- Core/SaveState.h | 8 ++--- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 4c1c223a7d..10739b20b9 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -136,24 +136,30 @@ void WebSocketGameStatus(DebuggerRequest &req) { // Parameters: // - path: string, path to the .ppst file. // -// Response (same event name) with no extra data, once queued. +// Response (same event name) with no extra data, once queued, or an error if the network or +// achievements hardcore mode refuses it. void WebSocketGameStateLoad(DebuggerRequest &req) { std::string path; if (!req.ParamString("path", &path)) return; bool running = false; + bool queued = false; Core_RunOnCPUThread([&] { running = PSP_GetBootState() == BootState::Complete; + if (running) { + queued = SaveState::Load(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { + if (status == SaveState::Status::FAILURE) { + ERROR_LOG(Log::System, "game.state.load: %.*s", STR_VIEW(message)); + } + }); + } }); if (!running) return req.Fail("Game not running"); - - SaveState::Load(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { - if (status == SaveState::Status::FAILURE) { - ERROR_LOG(Log::System, "game.state.load: %.*s", STR_VIEW(message)); - } - }); + if (!queued) { + return req.Fail("Could not queue savestate load"); + } req.Respond(); } @@ -164,24 +170,30 @@ void WebSocketGameStateLoad(DebuggerRequest &req) { // Parameters: // - path: string, path to write the .ppst file to. // -// Response (same event name) with no extra data, once queued. +// Response (same event name) with no extra data, once queued, or an error if the network or +// achievements hardcore mode refuses it. void WebSocketGameStateSave(DebuggerRequest &req) { std::string path; if (!req.ParamString("path", &path)) return; bool running = false; + bool queued = false; Core_RunOnCPUThread([&] { running = PSP_GetBootState() == BootState::Complete; + if (running) { + queued = SaveState::Save(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { + if (status == SaveState::Status::FAILURE) { + ERROR_LOG(Log::System, "game.state.save: %.*s", STR_VIEW(message)); + } + }); + } }); if (!running) return req.Fail("Game not running"); - - SaveState::Save(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { - if (status == SaveState::Status::FAILURE) { - ERROR_LOG(Log::System, "game.state.save: %.*s", STR_VIEW(message)); - } - }); + if (!queued) { + return req.Fail("Could not queue savestate save"); + } req.Respond(); } diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 6be636c0e7..630d231119 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -201,17 +201,17 @@ int g_screenshotFailures; pspFileSystem.DoState(p); } - void Enqueue(const SaveState::Operation &op) { + bool Enqueue(const SaveState::Operation &op) { // These two both refuse the operation, and both say so - dropping it silently just // looks like the hotkey didn't work. Callers that ask the same questions themselves // return before getting here, so nothing shows a message twice. if (NetworkWarnUserIfOnlineAndCantSavestate()) { - return; + return false; } // Hardcore mode bans loading savestates outright, and saving too unless the user opted // back into that. if (Achievements::WarnUserIfHardcoreModeActive(op.type == OperationType::Save)) { - return; + return false; } std::lock_guard guard(mutex); @@ -220,28 +220,29 @@ int g_screenshotFailures; // Don't actually run it until next frame. // It's possible there might be a duplicate but it won't hurt us. needsProcess = true; + return true; } - void Load(const Path &filename, int slot, Callback callback) { + bool Load(const Path &filename, int slot, Callback callback) { if (NetworkWarnUserIfOnlineAndCantSavestate()) { - return; + return false; } rewindStates.NotifyState(); if (coreState == CoreState::CORE_RUNTIME_ERROR) Core_Break(BreakReason::SavestateLoad, 0); - Enqueue(Operation(OperationType::Load, filename, slot, callback)); + return Enqueue(Operation(OperationType::Load, filename, slot, callback)); } - void Save(const Path &filename, int slot, Callback callback) { + bool Save(const Path &filename, int slot, Callback callback) { if (NetworkWarnUserIfOnlineAndCantSavestate()) { - return; + return false; } rewindStates.NotifyState(); if (coreState == CoreState::CORE_RUNTIME_ERROR) Core_Break(BreakReason::SavestateSave, 0); - Enqueue(Operation(OperationType::Save, filename, slot, callback)); + return Enqueue(Operation(OperationType::Save, filename, slot, callback)); } void Verify(Callback callback) { diff --git a/Core/SaveState.h b/Core/SaveState.h index 746c4c3f11..cfbda4ec1a 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -88,12 +88,12 @@ namespace SaveState { std::string GetTitle(const Path &filename); // Load the specified file into the current state (async.) - // Warning: callback will be called on a different thread. - void Load(const Path &filename, int slot, Callback callback = Callback()); + // Returns true if queued. The callback may run on a different thread. + bool Load(const Path &filename, int slot, Callback callback = Callback()); // Save the current state to the specified file (async.) - // Warning: callback will be called on a different thread. - void Save(const Path &filename, int slot, Callback callback = Callback()); + // Returns true if queued. The callback may run on a different thread. + bool Save(const Path &filename, int slot, Callback callback = Callback()); CChunkFileReader::Error SaveToRam(std::vector &state); CChunkFileReader::Error LoadFromRam(std::vector &state, std::string *errorString);