From 6600d1c05f9434cec7d63d0f01b7bd16bb5d4e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 12 Sep 2026 11:55:21 -0600 Subject: [PATCH] Savestate browser: delete the screenshot and name file along with the state Deleting a savestate from the savedata screen goes through GameInfo::Delete, not SaveState::DeleteSlot, and it only knew about the .jpg - so the slot's .name.txt was left behind with nothing to belong to. Rather than teach the UI the naming scheme a third time, SaveState now answers what sits beside a state. Co-Authored-By: Claude Opus 5 (1M context) --- Core/SaveState.cpp | 15 +++++++++++++++ Core/SaveState.h | 6 ++++++ UI/GameInfoCache.cpp | 8 +++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 1dfa5a86ee..810b82f0db 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -735,6 +735,21 @@ int g_screenshotFailures; Rescan(gamePrefix); } + std::vector GetCompanionFilePaths(const Path &statePath) { + const std::string stateExtension = std::string(".") + STATE_EXTENSION; + // A path that isn't a savestate yields nothing, which WithReplacedExtension now tells us + // rather than handing back the path itself. An undo state does work, and correctly: + // "x.undo.ppst" maps onto "x.undo.jpg". + std::vector paths; + for (const char *extension : { SCREENSHOT_EXTENSION, NAME_EXTENSION }) { + Path companion; + if (statePath.WithReplacedExtension(stateExtension, std::string(".") + extension, &companion)) { + paths.push_back(companion); + } + } + return paths; + } + std::vector Flush() { std::lock_guard guard(mutex); std::vector copy = g_pendingOperations; diff --git a/Core/SaveState.h b/Core/SaveState.h index b20a689c53..746c4c3f11 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -79,6 +79,12 @@ namespace SaveState { Path GenerateSaveSlotPath(std::string_view gamePrefix, int slot, const char *extension); void SetSlotCustomName(std::string_view gamePrefix, int slot, std::string_view newName); + // The files that sit beside a savestate and mean nothing without it: its screenshot, and the + // slot's custom name if it was given one. Anything deleting a .ppst from outside DeleteSlot - + // the savestate browser does exactly that - has to take these along, or they stay in the + // savestate folder for good. Returns nothing for a path that isn't a savestate. + std::vector GetCompanionFilePaths(const Path &statePath); + std::string GetTitle(const Path &filename); // Load the specified file into the current state (async.) diff --git a/UI/GameInfoCache.cpp b/UI/GameInfoCache.cpp index 5d353f2eb2..20fa67ae97 100644 --- a/UI/GameInfoCache.cpp +++ b/UI/GameInfoCache.cpp @@ -170,9 +170,11 @@ bool GameInfo::Delete() { const Path &ppstPath = filePath_; INFO_LOG(Log::System, "Deleting file %s", ppstPath.c_str()); MoveFileToTrashOrDelete(ppstPath); - Path screenshotPath; - if (filePath_.WithReplacedExtension(".ppst", ".jpg", &screenshotPath) && File::Exists(screenshotPath)) { - MoveFileToTrashOrDelete(screenshotPath); + // The screenshot and the slot's custom name are no use without the state itself. + for (const Path &companion : SaveState::GetCompanionFilePaths(ppstPath)) { + if (File::Exists(companion)) { + MoveFileToTrashOrDelete(companion); + } } return true; }