From afeab2718683bff80eb862a3db2308c2015ea73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 5 Sep 2026 16:15:37 -0600 Subject: [PATCH] Don't load RAIntegration from an install we can't write to RAIntegration keeps its cache and local achievement data next to the executable. If PPSSPP is installed somewhere that needs elevation to write - Program Files being the obvious case - that write fails and takes the emulator down as soon as a set or code notes are loaded, with no log to show for it since the log can't be written either. Check whether the exe directory is writable before handing the DLL to rcheevos, and if it isn't, say so and point at the portable .zip instead. Achievements themselves still work, so carry on to the normal login. Fixes #21260 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup --- Common/File/FileUtil.cpp | 12 ++++++++++++ Common/File/FileUtil.h | 4 ++++ Core/RetroAchievements.cpp | 16 ++++++++++++++++ assets/lang/en_US.ini | 1 + 4 files changed, 33 insertions(+) diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp index 62020ee59b..218669cd68 100644 --- a/Common/File/FileUtil.cpp +++ b/Common/File/FileUtil.cpp @@ -1068,6 +1068,18 @@ bool CreateEmptyFile(const Path &filename) { return true; } +bool IsDirectoryWritable(const Path &path) { + // There's no portable way to ask, so just try it and clean up after ourselves. + const Path probe = path / ".ppsspp_write_test"; + FILE *file = OpenCFile(probe, "wb"); + if (!file) { + return false; + } + fclose(file); + Delete(probe, true); + return true; +} + // Deletes an empty directory, returns true on success // WARNING: On Android with content URIs, it will delete recursively! bool DeleteDir(const Path &path) { diff --git a/Common/File/FileUtil.h b/Common/File/FileUtil.h index 054b6c9d33..9b5a0e12cf 100644 --- a/Common/File/FileUtil.h +++ b/Common/File/FileUtil.h @@ -123,6 +123,10 @@ bool MoveIfFast(const Path &srcFilename, const Path &destFilename); // creates an empty file filename, returns true on success bool CreateEmptyFile(const Path &filename); +// Can we actually create files in this directory? Checks by trying, since permission bits +// don't tell the whole story (Windows ACLs, read-only mounts, ...). +bool IsDirectoryWritable(const Path &path); + // Opens ini file (cheats, texture replacements etc.) // TODO: Belongs in System or something. bool OpenFileInEditor(const Path &fileName); diff --git a/Core/RetroAchievements.cpp b/Core/RetroAchievements.cpp index a85f2fc2bf..723252e21a 100644 --- a/Core/RetroAchievements.cpp +++ b/Core/RetroAchievements.cpp @@ -37,6 +37,7 @@ #include "Common/Crypto/md5.h" #include "Common/Log.h" #include "Common/File/Path.h" +#include "Common/File/FileUtil.h" #include "Common/Net/HTTPRequest.h" #include "Common/Net/HTTPClient.h" #include "Common/System/OSD.h" @@ -704,6 +705,21 @@ void InitializeRAIntegration(void *windowHandle) { ERROR_LOG(Log::Achievements, "RAIntegration is enabled, but no main window handle was found."); return; } + + // RAIntegration writes its cache and local achievement data next to the executable. If we + // can't write there - the usual case being an install under Program Files - it takes the + // emulator down with it as soon as it loads a set, so refuse to load it at all. See #21260. + const Path &exeDir = File::GetExeDirectory(); + if (!File::IsDirectoryWritable(exeDir)) { + auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); + ERROR_LOG(Log::Achievements, "Not loading RAIntegration, '%s' is not writable", exeDir.c_str()); + g_OSD.Show(OSDType::MESSAGE_ERROR, ac->T("RAIntegrationNotWritable", + "RAIntegration needs to write next to PPSSPP.exe, which this install doesn't allow. Use the portable .zip version instead."), "", g_RAImageID, 10.0f); + // Carry on without the toolkit - plain achievements still work. + TryLoginByToken(true); + return; + } + rc_client_begin_load_raintegration(g_rcClient, szFilePath, hWnd, "PPSSPP", PPSSPP_GIT_VERSION, &load_integration_callback, hWnd); return; } diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 5b8dd9a2f0..d0a33db0c3 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -67,6 +67,7 @@ Missable = Missable Notifications = Notifications Progression = Progression RAIntegration is enabled, but %1 was not found. = RAIntegration is enabled, but %1 was not found. +RAIntegrationNotWritable = RAIntegration needs to write next to PPSSPP.exe, which this install doesn't allow. Use the portable .zip version instead. Recently unlocked = Recently unlocked Reconnected to RetroAchievements. = Reconnected to RetroAchievements. Register on www.retroachievements.org = Register on www.retroachievements.org