From 9720041ecf2c1f22107cedab3ce0c7453738e3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 11 Oct 2026 13:49:08 +0200 Subject: [PATCH] Debugger: Add game.state.load Queues loading a savestate file, for scripting repros that load a state partway through a run. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Debugger/WebSocket/GameSubscriber.cpp | 31 ++++++++++++++++++++++ Core/Debugger/WebSocket/GameSubscriber.h | 1 + docs/WebSocketDebugger.md | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index a9c1c8508d..9cebba3038 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -38,6 +38,7 @@ #include "Core/ELF/ParamSFO.h" #include "Core/HLE/sceDisplay.h" #include "Core/RetroAchievements.h" +#include "Core/SaveState.h" #include "Core/System.h" // Declared rather than included from Core/HLE/sceNet.h, which reaches winsock and so windows.h @@ -58,6 +59,7 @@ static uint32_t GetOwnProcessID() { DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { map["game.reset"] = &WebSocketGameReset; map["game.status"] = &WebSocketGameStatus; + map["game.state.load"] = &WebSocketGameStateLoad; map["game.speed.get"] = &WebSocketGameSpeedGet; map["game.speed.set"] = &WebSocketGameSpeedSet; map["version"] = &WebSocketVersion; @@ -125,6 +127,35 @@ void WebSocketGameStatus(DebuggerRequest &req) { }); } +// Load a savestate file (game.state.load) +// +// The load is queued, and happens at the end of the next frame the CPU runs, so follow this with +// something that runs it, like cpu.runUntilTime. +// +// Parameters: +// - path: string, path to the .ppst file. +// +// Response (same event name) with no extra data, once queued. +void WebSocketGameStateLoad(DebuggerRequest &req) { + std::string path; + if (!req.ParamString("path", &path)) + return; + + bool running = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + }); + 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)); + } + }); + req.Respond(); +} + // Percentages are relative to 60 FPS, matching how the in-app settings present the alternative // speeds (see GameSettingsScreen.cpp) - so a client and the UI agree on what "200%" means. static int PercentToFps(int percent) { diff --git a/Core/Debugger/WebSocket/GameSubscriber.h b/Core/Debugger/WebSocket/GameSubscriber.h index 68943c1af7..1e340c5fdf 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.h +++ b/Core/Debugger/WebSocket/GameSubscriber.h @@ -23,6 +23,7 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map); void WebSocketGameReset(DebuggerRequest &req); void WebSocketGameStatus(DebuggerRequest &req); +void WebSocketGameStateLoad(DebuggerRequest &req); void WebSocketGameSpeedGet(DebuggerRequest &req); void WebSocketGameSpeedSet(DebuggerRequest &req); void WebSocketVersion(DebuggerRequest &req); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 7ad9503a3d..1a1379c27a 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -270,7 +270,7 @@ file - this is just an index. | Category | Events | File | |---|---|---| -| Game/version | `game.reset`, `game.status`, `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | +| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file, applied at the end of the next frame the CPU runs), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |