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