From 179c2475a744078ab89011ba5e02f45796036115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 7 Nov 2025 22:44:23 -0500 Subject: [PATCH 01/20] Use the libretro VFS interface in libretro builds --- CMakeLists.txt | 2 + src/filesystem_root.cpp | 15 ++ src/platform/libretro/filesystem_libretro.cpp | 195 ++++++++++++++++++ src/platform/libretro/filesystem_libretro.h | 56 +++++ src/platform/libretro/ui.cpp | 9 + 5 files changed, 277 insertions(+) create mode 100644 src/platform/libretro/filesystem_libretro.cpp create mode 100644 src/platform/libretro/filesystem_libretro.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9707ab6d9e..a66e526ed7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1449,6 +1449,8 @@ else() # library src/platform/libretro/audio.h src/platform/libretro/clock.cpp src/platform/libretro/clock.h + src/platform/libretro/filesystem_libretro.cpp + src/platform/libretro/filesystem_libretro.h src/platform/libretro/input_buttons.cpp src/platform/libretro/ui.cpp src/platform/libretro/ui.h diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index 4cb334590c..8f569a3506 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -23,6 +23,10 @@ # include "platform/android/filesystem_saf.h" #endif +#ifdef USE_LIBRETRO +# include "platform/libretro/filesystem_libretro.h" +#endif + constexpr const std::string_view root_ns = "root://"; RootFilesystem::RootFilesystem() : Filesystem("", FilesystemView()) { @@ -32,6 +36,10 @@ RootFilesystem::RootFilesystem() : Filesystem("", FilesystemView()) { fs_list.push_back(std::make_pair("content", std::make_unique("", FilesystemView()))); #endif +#ifdef USE_LIBRETRO + fs_list.push_back(std::make_pair("libretro", std::make_unique("", FilesystemView()))); +#endif + // IMPORTANT: This must be the last filesystem in the list, do not push anything to fs_list afterwards! fs_list.push_back(std::make_pair("file", std::make_unique("", FilesystemView()))); @@ -106,12 +114,19 @@ const Filesystem& RootFilesystem::FilesystemForPath(std::string_view path) const assert(!fs_list.empty()); std::string_view ns; + +#ifdef USE_LIBRETRO + if (LibretroFilesystem::vfs.required_interface_version >= EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION) { + ns = "libretro"; + } +#else // Check if the path contains a namespace auto ns_pos = path.find("://"); if (ns_pos != std::string::npos) { ns = path.substr(0, ns_pos); path = path.substr(ns_pos + 3); } +#endif if (ns.empty()) { // No namespace returns the last fs which is the NativeFilesystem diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp new file mode 100644 index 0000000000..0bcd463ea2 --- /dev/null +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -0,0 +1,195 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "filesystem_libretro.h" +#include "filesystem_stream.h" +#include "output.h" + +struct retro_vfs_interface_info LibretroFilesystem::vfs; + +LibretroFilesystem::LibretroFilesystem(std::string base_path, FilesystemView parent_fs) : Filesystem(std::move(base_path), parent_fs) { +} + +bool LibretroFilesystem::IsFile(std::string_view path) const { + int flags = vfs.iface->stat(ToString(path).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID && !(flags & RETRO_VFS_STAT_IS_DIRECTORY); +} + +bool LibretroFilesystem::IsDirectory(std::string_view dir, bool) const { + int flags = vfs.iface->stat(ToString(dir).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID && flags & RETRO_VFS_STAT_IS_DIRECTORY; +} + +bool LibretroFilesystem::Exists(std::string_view filename) const { + int flags = vfs.iface->stat(ToString(filename).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID; +} + +int64_t LibretroFilesystem::GetFilesize(std::string_view path) const { + int32_t size; + int flags = vfs.iface->stat(ToString(path).c_str(), &flags); + return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; +} + +class LibretroStreamBufIn : public std::streambuf { +public: + LibretroStreamBufIn(struct retro_vfs_file_handle* handle) : std::streambuf(), handle(handle) { + setg(buffer_start, buffer_start, buffer_start); + } + + ~LibretroStreamBufIn() override { + LibretroFilesystem::vfs.iface->close(handle); + } + + int underflow() override { + ssize_t res = LibretroFilesystem::vfs.iface->read(handle, buffer.data(), buffer.size()); + if (res == 0) { + return traits_type::eof(); + } else if (res < 0) { + Output::Debug("underflow failed: {}", strerror(errno)); + return traits_type::eof(); + } + setg(buffer_start, buffer_start, buffer_start + res); + return traits_type::to_int_type(*gptr()); + } + + std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode mode) override { + if (dir == std::ios_base::cur) { + offset += static_cast(gptr() - egptr()); + } + int cdir = Filesystem_Stream::CppSeekdirToCSeekdir(dir); + auto res = LibretroFilesystem::vfs.iface->seek(handle, offset, cdir == SEEK_CUR ? RETRO_VFS_SEEK_POSITION_CURRENT : cdir == SEEK_END ? RETRO_VFS_SEEK_POSITION_END : RETRO_VFS_SEEK_POSITION_START); + setg(buffer_start, buffer_end, buffer_end); + return res; + } + + std::streambuf::pos_type seekpos(std::streambuf::pos_type pos, std::ios_base::openmode mode) override { + return LibretroFilesystem::vfs.iface->tell(handle); + } + +private: + struct retro_vfs_file_handle* handle; + std::array buffer; + char* buffer_start = &buffer.front(); + char* buffer_end = &buffer.back(); +}; + +std::streambuf* LibretroFilesystem::CreateInputStreambuffer(std::string_view path, std::ios_base::openmode) const { + struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); +} + +class LibretroStreamBufOut : public std::streambuf { +public: + LibretroStreamBufOut(struct retro_vfs_file_handle* handle) : std::streambuf(), handle(handle) { + setp(buffer_start, buffer_end); + } + + ~LibretroStreamBufOut() override { + sync(); + LibretroFilesystem::vfs.iface->close(handle); + } + + int overflow(int c = EOF) override { + if (sync() < 0) { + return traits_type::eof(); + } + if (c != EOF) { + char a = static_cast(c); + ssize_t res = LibretroFilesystem::vfs.iface->write(handle, &a, 1); + if (res < 1) { + return traits_type::eof(); + } + } + + return c; + } + + int sync() override { + auto len = pptr() - pbase(); + if (len == 0) { + return 0; + } + ssize_t res = LibretroFilesystem::vfs.iface->write(handle, pbase(), len); + setp(buffer_start, buffer_end); + if (res < len) { + return -1; + } + return 0; + } + +private: + struct retro_vfs_file_handle* handle; + std::array buffer; + char* buffer_start = &buffer.front(); + char* buffer_end = &buffer.back(); +}; + +std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode) const { + struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); +} + +// To prevent leaking of the directory handle if an exception is thrown within GetDirectoryContent +class LibretroDirGuard { +public: + LibretroDirGuard(struct retro_vfs_dir_handle* handle) : handle(handle) { + } + + ~LibretroDirGuard() { + LibretroFilesystem::vfs.iface->closedir(handle); + } + +private: + struct retro_vfs_dir_handle* handle; +}; + +bool LibretroFilesystem::GetDirectoryContent(std::string_view path, std::vector& entries) const { + std::string p = ToString(path); + + struct retro_vfs_dir_handle* dir = vfs.iface->opendir(p.c_str(), true); + if (dir == nullptr) { + Output::Debug("Error opening dir {}", p); + return false; + } + LibretroDirGuard guard(dir); + + while (vfs.iface->readdir(dir)) { + const char* name = vfs.iface->dirent_get_name(dir); + if (name == nullptr) { + continue; + } + bool is_directory = vfs.iface->dirent_is_dir(dir); + entries.emplace_back( + name, + is_directory ? DirectoryTree::FileType::Directory : DirectoryTree::FileType::Regular); + } + + return true; +} + +bool LibretroFilesystem::MakeDirectory(std::string_view path, bool) const { + return vfs.iface->mkdir(ToString(path).c_str()) >= 0; +} + +bool LibretroFilesystem::IsFeatureSupported(Feature f) const { + return f == Filesystem::Feature::Write; +} + +std::string LibretroFilesystem::Describe() const { + return fmt::format("[libretro] {}", GetPath()); +} diff --git a/src/platform/libretro/filesystem_libretro.h b/src/platform/libretro/filesystem_libretro.h new file mode 100644 index 0000000000..60ee7da0c7 --- /dev/null +++ b/src/platform/libretro/filesystem_libretro.h @@ -0,0 +1,56 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_FILESYSTEM_LIBRETRO_H +#define EP_FILESYSTEM_LIBRETRO_H + +#include "filesystem.h" +#include "libretro.h" + +#define EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION 3U + +/** + * A wrapper around the libretro virtual filesystem interface + */ +class LibretroFilesystem : public Filesystem { +public: + /** + * Initializes a libretro filesystem + */ + explicit LibretroFilesystem(std::string base_path, FilesystemView parent_fs); + + static struct retro_vfs_interface_info vfs; + +protected: + /** + * Implementation of abstract methods + */ + /** @{ */ + bool IsFile(std::string_view path) const override; + bool IsDirectory(std::string_view path, bool follow_symlinks) const override; + bool Exists(std::string_view path) const override; + int64_t GetFilesize(std::string_view path) const override; + std::streambuf* CreateInputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; + std::streambuf* CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; + bool GetDirectoryContent(std::string_view path, std::vector& entries) const override; + bool MakeDirectory(std::string_view path, bool follow_symlinks) const override; + bool IsFeatureSupported(Feature f) const override; + std::string Describe() const override; + /** @} */ +}; + +#endif diff --git a/src/platform/libretro/ui.cpp b/src/platform/libretro/ui.cpp index e05087ed97..dcde4430f4 100644 --- a/src/platform/libretro/ui.cpp +++ b/src/platform/libretro/ui.cpp @@ -18,6 +18,7 @@ // Headers #include "ui.h" #include "clock.h" +#include "filesystem_libretro.h" #include "bitmap.h" #include "color.h" #include "filefinder.h" @@ -316,6 +317,14 @@ RETRO_API void retro_set_environment(retro_environment_t cb) { { nullptr, nullptr } }; cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); + + if (LibretroFilesystem::vfs.required_interface_version < EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION) { + LibretroFilesystem::vfs.required_interface_version = EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION; + if (!cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &LibretroFilesystem::vfs)) { + LibretroFilesystem::vfs.required_interface_version = 0; + LibretroFilesystem::vfs.iface = nullptr; + } + } } RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb) { From 7a48203c575905b0b38945282c13615085c79a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 09:51:25 -0500 Subject: [PATCH 02/20] Fix seekpos implementation in filesystem_libretro.cpp --- src/platform/libretro/filesystem_libretro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index 0bcd463ea2..eaf7cf7d0c 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -78,7 +78,7 @@ class LibretroStreamBufIn : public std::streambuf { } std::streambuf::pos_type seekpos(std::streambuf::pos_type pos, std::ios_base::openmode mode) override { - return LibretroFilesystem::vfs.iface->tell(handle); + return seekoff(pos, std::ios_base::beg, mode); } private: From a553f652fb4dc00b7cd65c90e072b7b8bb3ab238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 09:53:19 -0500 Subject: [PATCH 03/20] Improve code for getting the libretro VFS --- src/platform/libretro/ui.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/platform/libretro/ui.cpp b/src/platform/libretro/ui.cpp index dcde4430f4..d06ca1f311 100644 --- a/src/platform/libretro/ui.cpp +++ b/src/platform/libretro/ui.cpp @@ -318,12 +318,10 @@ RETRO_API void retro_set_environment(retro_environment_t cb) { }; cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); - if (LibretroFilesystem::vfs.required_interface_version < EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION) { - LibretroFilesystem::vfs.required_interface_version = EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION; - if (!cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &LibretroFilesystem::vfs)) { - LibretroFilesystem::vfs.required_interface_version = 0; - LibretroFilesystem::vfs.iface = nullptr; - } + struct retro_vfs_interface_info vfs; + vfs.required_interface_version = EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION; + if (cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs)) { + LibretroFilesystem::vfs = vfs; } } From 52b639314ae05c4b23da3bb0228fa0cb8485068f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 12:59:56 -0500 Subject: [PATCH 04/20] Handle append mode in `LibretroFilesystem::CreateOutputStreambuffer()` --- src/platform/libretro/filesystem_libretro.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index eaf7cf7d0c..e0e88e4a10 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -139,9 +139,21 @@ class LibretroStreamBufOut : public std::streambuf { char* buffer_end = &buffer.back(); }; -std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode) const { - struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); - return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); +std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const { + if ((mode & std::ios_base::app) == std::ios_base::app && Exists(path)) { + struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (handle == nullptr) { + return nullptr; + } + if (vfs.iface->seek(handle, 0, RETRO_VFS_SEEK_POSITION_END) == -1) { + vfs.iface->close(handle); + return nullptr; + } + return new LibretroStreamBufIn(handle); + } else { + struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); + } } // To prevent leaking of the directory handle if an exception is thrown within GetDirectoryContent From ed6f97ef311133fc72d0adbabb8f1ea593374c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 13:04:42 -0500 Subject: [PATCH 05/20] Don't return false in `LibretroFilesystem::MakeDirectory()` if the directory already exists --- src/platform/libretro/filesystem_libretro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index e0e88e4a10..347461927a 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -195,7 +195,7 @@ bool LibretroFilesystem::GetDirectoryContent(std::string_view path, std::vector< } bool LibretroFilesystem::MakeDirectory(std::string_view path, bool) const { - return vfs.iface->mkdir(ToString(path).c_str()) >= 0; + return vfs.iface->mkdir(ToString(path).c_str()) != -1; } bool LibretroFilesystem::IsFeatureSupported(Feature f) const { From 04e6b34f619165eeecbefac5eff4912f67765002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 15:02:03 -0500 Subject: [PATCH 06/20] Replace `ssize_t` with `int64_t` in filesystem_libretro.cpp to fix x64 Windows builds --- src/platform/libretro/filesystem_libretro.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index 347461927a..bf92f3eacc 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -56,7 +56,7 @@ class LibretroStreamBufIn : public std::streambuf { } int underflow() override { - ssize_t res = LibretroFilesystem::vfs.iface->read(handle, buffer.data(), buffer.size()); + int64_t res = LibretroFilesystem::vfs.iface->read(handle, buffer.data(), buffer.size()); if (res == 0) { return traits_type::eof(); } else if (res < 0) { @@ -110,7 +110,7 @@ class LibretroStreamBufOut : public std::streambuf { } if (c != EOF) { char a = static_cast(c); - ssize_t res = LibretroFilesystem::vfs.iface->write(handle, &a, 1); + int64_t res = LibretroFilesystem::vfs.iface->write(handle, &a, 1); if (res < 1) { return traits_type::eof(); } @@ -124,7 +124,7 @@ class LibretroStreamBufOut : public std::streambuf { if (len == 0) { return 0; } - ssize_t res = LibretroFilesystem::vfs.iface->write(handle, pbase(), len); + int64_t res = LibretroFilesystem::vfs.iface->write(handle, pbase(), len); setp(buffer_start, buffer_end); if (res < len) { return -1; From 2853c2fde07c7e90442f80cd83ca5fe24138858b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 8 Nov 2025 15:32:10 -0500 Subject: [PATCH 07/20] Fix a typo in `LibretroFilesystem::GetFilesize()` --- src/platform/libretro/filesystem_libretro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index bf92f3eacc..c60bebc0fd 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -41,7 +41,7 @@ bool LibretroFilesystem::Exists(std::string_view filename) const { int64_t LibretroFilesystem::GetFilesize(std::string_view path) const { int32_t size; - int flags = vfs.iface->stat(ToString(path).c_str(), &flags); + int flags = vfs.iface->stat(ToString(path).c_str(), &size); return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; } From 63129c38bd6f9f204001422d2c0447d2ba28819f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 12 Nov 2025 12:33:11 -0500 Subject: [PATCH 08/20] Don't leak the file handle if an exception is thrown while creating a `LibretroFilesystem` file stream --- src/platform/libretro/filesystem_libretro.cpp | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index c60bebc0fd..ad22f578f1 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -45,6 +45,26 @@ int64_t LibretroFilesystem::GetFilesize(std::string_view path) const { return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; } +// To prevent leaking of the file handle if an exception is thrown within CreateInputStreambuffer/CreateOutputStreambuffer +class LibretroFileGuard { +public: + LibretroFileGuard(struct retro_vfs_file_handle* handle) noexcept : handle(handle) { + } + + ~LibretroFileGuard() { + if (handle != nullptr) { + LibretroFilesystem::vfs.iface->close(handle); + } + } + + void forget() noexcept { + handle = nullptr; + } + +private: + struct retro_vfs_file_handle* handle; +}; + class LibretroStreamBufIn : public std::streambuf { public: LibretroStreamBufIn(struct retro_vfs_file_handle* handle) : std::streambuf(), handle(handle) { @@ -90,7 +110,13 @@ class LibretroStreamBufIn : public std::streambuf { std::streambuf* LibretroFilesystem::CreateInputStreambuffer(std::string_view path, std::ios_base::openmode) const { struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); - return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); + if (handle == nullptr) { + return nullptr; + } + LibretroFileGuard guard(handle); + LibretroStreamBufIn* stream = new LibretroStreamBufIn(handle); + guard.forget(); + return stream; } class LibretroStreamBufOut : public std::streambuf { @@ -140,8 +166,9 @@ class LibretroStreamBufOut : public std::streambuf { }; std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const { + struct retro_vfs_file_handle* handle; if ((mode & std::ios_base::app) == std::ios_base::app && Exists(path)) { - struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, RETRO_VFS_FILE_ACCESS_HINT_NONE); + handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, RETRO_VFS_FILE_ACCESS_HINT_NONE); if (handle == nullptr) { return nullptr; } @@ -149,17 +176,22 @@ std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view pa vfs.iface->close(handle); return nullptr; } - return new LibretroStreamBufIn(handle); } else { - struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); - return handle == nullptr ? nullptr : new LibretroStreamBufIn(handle); + handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (handle == nullptr) { + return nullptr; + } } + LibretroFileGuard guard(handle); + LibretroStreamBufIn* stream = new LibretroStreamBufIn(handle); + guard.forget(); + return stream; } // To prevent leaking of the directory handle if an exception is thrown within GetDirectoryContent class LibretroDirGuard { public: - LibretroDirGuard(struct retro_vfs_dir_handle* handle) : handle(handle) { + LibretroDirGuard(struct retro_vfs_dir_handle* handle) noexcept : handle(handle) { } ~LibretroDirGuard() { From 0250dd1b6cb9efaff8bc5d2c0528ecb08c529840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 12 Nov 2025 12:44:18 -0500 Subject: [PATCH 09/20] Fix a typo in the previous commit --- src/platform/libretro/filesystem_libretro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index ad22f578f1..5532fd066b 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -183,7 +183,7 @@ std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view pa } } LibretroFileGuard guard(handle); - LibretroStreamBufIn* stream = new LibretroStreamBufIn(handle); + LibretroStreamBufOut* stream = new LibretroStreamBufOut(handle); guard.forget(); return stream; } From 04c6070ce633e39891face8b67610cb4598fd094 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 16:50:37 +0200 Subject: [PATCH 10/20] Update libretro common to dfccc5 --- builds/libretro/libretro-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/libretro/libretro-common b/builds/libretro/libretro-common index 20a43ba79f..dfccc5330a 160000 --- a/builds/libretro/libretro-common +++ b/builds/libretro/libretro-common @@ -1 +1 @@ -Subproject commit 20a43ba79fe6b4ec094b3b20b7bc88f4cfe916fa +Subproject commit dfccc5330a8e80ff8edf4f88902011d96380587d From 315372ac39637edc40b2f4378162ed020e21e9c8 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 16:51:11 +0200 Subject: [PATCH 11/20] Fix warnings --- src/game_ineluki.cpp | 1 + src/platform/android/filesystem_saf.cpp | 2 +- src/platform/libretro/filesystem_libretro.cpp | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game_ineluki.cpp b/src/game_ineluki.cpp index ad5c0c0a1e..110de9834a 100644 --- a/src/game_ineluki.cpp +++ b/src/game_ineluki.cpp @@ -116,6 +116,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { key_support = Utils::LowerCase(cmd.arg) == "true"; #if !defined(SUPPORT_KEYBOARD) + (void)prev_key_support; if (key_support) { Output::Warning("Ineluki: Keyboard input is not supported on this platform"); } diff --git a/src/platform/android/filesystem_saf.cpp b/src/platform/android/filesystem_saf.cpp index bd89e28f66..6d4ebbaf01 100644 --- a/src/platform/android/filesystem_saf.cpp +++ b/src/platform/android/filesystem_saf.cpp @@ -117,7 +117,7 @@ class FdStreamBufIn : public std::streambuf { return traits_type::to_int_type(*gptr()); } - std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode mode) override { + std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode /*mode*/) override { if (dir == std::ios_base::cur) { offset += static_cast(gptr() - egptr()); } diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index 5532fd066b..0aea4f556d 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -87,7 +87,7 @@ class LibretroStreamBufIn : public std::streambuf { return traits_type::to_int_type(*gptr()); } - std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode mode) override { + std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode /*mode*/) override { if (dir == std::ios_base::cur) { offset += static_cast(gptr() - egptr()); } From 3de2575058c49f111bcd3ff1f4c8de413ef9711d Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 17:25:46 +0200 Subject: [PATCH 12/20] Filesystem: Don't strip the namespace when libretro is used libretro requires them for correct functionality --- src/filesystem_root.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index 8f569a3506..d5ed4bafa4 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -57,11 +57,13 @@ FilesystemView RootFilesystem::Create(std::string_view path) const { } const auto& fs = FilesystemForPath(path); - // Strip namespace from path + // Strip namespace from path except for libretro which expects them +#ifdef USE_LIBRETRO auto ns_pos = path.find("://"); if (ns_pos != std::string::npos) { path = path.substr(ns_pos + 3); } +#endif return fs.Create(path); } From 62c5aeae386b9130fde61b7070c2f031366654f5 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 17:08:47 +0200 Subject: [PATCH 13/20] Move recursive directory creation code into the Filesystem class Our code assumes that this works at far too many places --- src/filesystem.cpp | 52 +++++++++++++++- src/filesystem.h | 17 ++++-- src/filesystem_hook.cpp | 2 +- src/filesystem_hook.h | 2 +- src/filesystem_native.cpp | 2 +- src/filesystem_native.h | 2 +- src/filesystem_root.cpp | 2 +- src/filesystem_root.h | 2 +- src/platform.cpp | 59 +++---------------- src/platform.h | 4 +- src/platform/libretro/filesystem_libretro.cpp | 2 +- src/platform/libretro/filesystem_libretro.h | 2 +- 12 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/filesystem.cpp b/src/filesystem.cpp index b292328820..810f0bdfba 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -184,7 +184,57 @@ FilesystemView Filesystem::Subtree(std::string sub_path) const { return FilesystemView(shared_from_this(), sub_path); } -bool Filesystem::MakeDirectory(std::string_view, bool) const { +bool Filesystem::MakeDirectory(std::string_view path, bool follow_symlinks) const { + if (IsDirectory(path, follow_symlinks)) { + return true; + } + + auto components = FileFinder::SplitPath(path); + std::string cur_path; + if (StartsWith(path, "/")) { + cur_path += "/"; + } + + bool first = true; + for (const auto& comp : components) { + if (comp.empty() || comp == ".") { + continue; + } + + cur_path = FileFinder::MakePath(cur_path, comp); + + if (first) { + // Do not check stuff that looks like drives, such as C:, ux0: or sd: + // Some systems do not consider them directories + first = false; + if (comp.back() == ':') { + continue; + } + } + +#if defined(__WIIU__) + if (cur_path == "fs:/vol" || cur_path == "/vol") { + // /vol is part of the path but checking for existance fails + continue; + } +#endif + + if (IsDirectory(cur_path, follow_symlinks)) { + continue; + } else if (Exists(cur_path)) { + // Exists but not a directory + return false; + } else { + if (!vMakeDirectory(cur_path, follow_symlinks)) { + return false; + } + } + } + + return true; +} + +bool Filesystem::vMakeDirectory(std::string_view, bool) const { return false; } diff --git a/src/filesystem.h b/src/filesystem.h index eb4bc3ac1e..2bb4f7cd06 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -22,9 +22,6 @@ #include #include #include -#include -#include -#include #include #include "directory_tree.h" @@ -211,6 +208,16 @@ class Filesystem : public std::enable_shared_from_this { /** Implicit conversion to FilesystemView */ operator FilesystemView(); + /** + * Recursively creates a new directory. + * Not all filesystems support directory creation. + * + * @param dir Directory to create. + * @param follow_symlinks Whether to follow symlinks (if supported by this filesystem) + * @return true when the directory was created or already exists. + */ + bool MakeDirectory(std::string_view path, bool follow_symlinks) const; + /** * Abstract methods to be implemented by filesystems. */ @@ -219,7 +226,7 @@ class Filesystem : public std::enable_shared_from_this { virtual bool IsDirectory(std::string_view path, bool follow_symlinks) const = 0; virtual bool Exists(std::string_view path) const = 0; virtual int64_t GetFilesize(std::string_view path) const = 0; - virtual bool MakeDirectory(std::string_view dir, bool follow_symlinks) const; + virtual bool vMakeDirectory(std::string_view path, bool follow_symlinks) const; virtual bool IsFeatureSupported(Feature f) const; virtual std::string Describe() const = 0; /** @} */ @@ -457,7 +464,7 @@ class FilesystemView { * * @param dir Directory to create. * @param follow_symlinks Whether to follow symlinks (if supported by this filesystem) - * @return true when the path was created + * @return true when the directory was created or already exists. */ bool MakeDirectory(std::string_view dir, bool follow_symlinks) const; diff --git a/src/filesystem_hook.cpp b/src/filesystem_hook.cpp index fe9be95299..f585511e3c 100644 --- a/src/filesystem_hook.cpp +++ b/src/filesystem_hook.cpp @@ -102,7 +102,7 @@ int64_t HookFilesystem::GetFilesize(std::string_view path) const { return GetParent().GetFilesize(path); } -bool HookFilesystem::MakeDirectory(std::string_view dir, bool follow_symlinks) const { +bool HookFilesystem::vMakeDirectory(std::string_view dir, bool follow_symlinks) const { return GetParent().MakeDirectory(dir, follow_symlinks); } diff --git a/src/filesystem_hook.h b/src/filesystem_hook.h index 8557198e82..5b3f475bfe 100644 --- a/src/filesystem_hook.h +++ b/src/filesystem_hook.h @@ -43,7 +43,7 @@ class HookFilesystem : public Filesystem { bool IsDirectory(std::string_view path, bool follow_symlinks) const override; bool Exists(std::string_view path) const override; int64_t GetFilesize(std::string_view path) const override; - bool MakeDirectory(std::string_view dir, bool follow_symlinks) const override; + bool vMakeDirectory(std::string_view dir, bool follow_symlinks) const override; bool IsFeatureSupported(Feature f) const override; std::string Describe() const override; /** @} */ diff --git a/src/filesystem_native.cpp b/src/filesystem_native.cpp index 758f7b6c10..f2df19fda5 100644 --- a/src/filesystem_native.cpp +++ b/src/filesystem_native.cpp @@ -148,7 +148,7 @@ bool NativeFilesystem::GetDirectoryContent(std::string_view path, std::vector& entries) const override; - bool MakeDirectory(std::string_view path, bool follow_symlinks) const override; + bool vMakeDirectory(std::string_view path, bool follow_symlinks) const override; bool IsFeatureSupported(Feature f) const override; std::string Describe() const override; /** @} */ diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index d5ed4bafa4..0be7d24fbd 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -104,7 +104,7 @@ bool RootFilesystem::GetDirectoryContent(std::string_view path, std::vector& entries) const override; - bool MakeDirectory(std::string_view path, bool follow_symlinks) const override; + bool vMakeDirectory(std::string_view path, bool follow_symlinks) const override; std::string Describe() const override; /** @} */ diff --git a/src/platform.cpp b/src/platform.cpp index c29241984a..59a9f73554 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -129,63 +129,20 @@ bool Platform::File::MakeDirectory(bool follow_symlinks) const { } #ifdef _WIN32 - std::string path = Utils::FromWideString(filename); -#else - std::string path = filename; -#endif - - auto components = FileFinder::SplitPath(path); - std::string cur_path; - if (StartsWith(path, "/")) { - cur_path += "/"; + if (!CreateDirectoryW(filename.c_str(), nullptr)) { + return false; } - - bool first = true; - for (const auto& comp : components) { - if (comp.empty() || comp == ".") { - continue; - } - - cur_path = FileFinder::MakePath(cur_path, comp); - - if (first) { - // Do not check stuff that looks like drives, such as C:, ux0: or sd: - // Some systems do not consider them directories - first = false; - if (comp.back() == ':') { - continue; - } - } - -#if defined(__WIIU__) - if (cur_path == "fs:/vol" || cur_path == "/vol") { - // /vol is part of the path but checking for existance fails - continue; - } -#endif - - File cf(cur_path); - if (cf.IsDirectory(follow_symlinks)) { - continue; - } else if (cf.IsFile(follow_symlinks) || cf.Exists()) { - return false; - } else { -#ifdef _WIN32 - if (!CreateDirectoryW(Utils::ToWideString(cur_path).c_str(), nullptr)) { - return false; - } #else # if defined(__vita__) - int res = sceIoMkdir(cur_path.c_str(), 0777); + int res = sceIoMkdir(filename.c_str(), 0777); # else - int res = mkdir(cur_path.c_str(), 0777); + int res = mkdir(filename.c_str(), 0777); # endif - if (res < 0) { - return false; - } -#endif - } + if (res < 0) { + return false; } +#endif + return true; } diff --git a/src/platform.h b/src/platform.h index a285822c7f..262bf2b029 100644 --- a/src/platform.h +++ b/src/platform.h @@ -90,9 +90,9 @@ namespace Platform { int64_t GetSize() const; /** - * Creates a directory recursively at the filename path. + * Creates the directory at the filename path. * @param follow_symlinks Whether to follow symlinks (if supported on this platform) - * @return true when the directory was created. + * @return true when the directory was created or already exists. */ bool MakeDirectory(bool follow_symlinks) const; diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index 0aea4f556d..dc44c71575 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -226,7 +226,7 @@ bool LibretroFilesystem::GetDirectoryContent(std::string_view path, std::vector< return true; } -bool LibretroFilesystem::MakeDirectory(std::string_view path, bool) const { +bool LibretroFilesystem::vMakeDirectory(std::string_view path, bool) const { return vfs.iface->mkdir(ToString(path).c_str()) != -1; } diff --git a/src/platform/libretro/filesystem_libretro.h b/src/platform/libretro/filesystem_libretro.h index 60ee7da0c7..08693634b4 100644 --- a/src/platform/libretro/filesystem_libretro.h +++ b/src/platform/libretro/filesystem_libretro.h @@ -47,7 +47,7 @@ class LibretroFilesystem : public Filesystem { std::streambuf* CreateInputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; std::streambuf* CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; bool GetDirectoryContent(std::string_view path, std::vector& entries) const override; - bool MakeDirectory(std::string_view path, bool follow_symlinks) const override; + bool vMakeDirectory(std::string_view path, bool follow_symlinks) const override; bool IsFeatureSupported(Feature f) const override; std::string Describe() const override; /** @} */ From 85c19d6a50745d61776cb6f5b9ed1bb2adf381d1 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 17:09:44 +0200 Subject: [PATCH 14/20] Android: Add folder creation to SAF Fix #3593 --- .../org/easyrpg/player/player/SafFile.java | 23 +++++++++++++++++++ src/game_config.cpp | 2 -- src/platform/android/filesystem_saf.cpp | 14 +++++++++++ src/platform/android/filesystem_saf.h | 1 + 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/player/SafFile.java b/builds/android/app/src/main/java/org/easyrpg/player/player/SafFile.java index 2b235658e8..30c7d3d62d 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/player/SafFile.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/player/SafFile.java @@ -92,6 +92,29 @@ public long getFilesize() { return metaFileSize; } + public boolean makeDirectory() { + if (exists()) { + return isDirectory(); + } + + // To create it the parent directory must be obtained + String full_path = rootUri.toString(); + int last_slash = full_path.lastIndexOf("%2F"); + if (last_slash == -1) { + return false; + } + String directory = full_path.substring(0, last_slash); + String filename = full_path.substring(last_slash + 3); + filename = Uri.decode(filename); + + DocumentFile df = DocumentFile.fromTreeUri(context, Uri.parse(directory)); + if (df == null || !df.exists()) { + return false; + } + df = df.createDirectory(filename); + return df != null && df.exists(); + } + public int createInputFileDescriptor() { // No difference between read mode and binary read mode try (ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(rootUri, "r")) { diff --git a/src/game_config.cpp b/src/game_config.cpp index 9d530ccbf6..f30e81c36a 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -347,13 +347,11 @@ Filesystem_Stream::OutputStream& Game_Config::GetLogFileOutput() { return noop_stream; } -#ifndef ANDROID // Make Directory not supported on Android, assume the path exists if (!FileFinder::Root().MakeDirectory(FileFinder::GetPathAndFilename(path).first, true)) { print_err(); return noop_stream; } -#endif logging.handle = FileFinder::Root().OpenOutputStream(path, std::ios_base::out | std::ios_base::app); diff --git a/src/platform/android/filesystem_saf.cpp b/src/platform/android/filesystem_saf.cpp index 6d4ebbaf01..770f0293b3 100644 --- a/src/platform/android/filesystem_saf.cpp +++ b/src/platform/android/filesystem_saf.cpp @@ -95,6 +95,20 @@ int64_t SafFilesystem::GetFilesize(std::string_view path) const { return static_cast(res); } +bool SafFilesystem::vMakeDirectory(std::string_view path, bool) const { + auto obj = get_jni_handle(this, path); + if (!obj) { + return false; + } + + JNIEnv* env = EpAndroid::env; + jclass cls = env->GetObjectClass(obj); + jmethodID jni_method = env->GetMethodID(cls, "makeDirectory", "()Z"); + jboolean res = env->CallBooleanMethod(obj, jni_method); + + return res > 0; +} + class FdStreamBufIn : public std::streambuf { public: FdStreamBufIn(int fd, std::array buffer, ssize_t bytes_read) : std::streambuf(), fd(fd), buffer(buffer) { diff --git a/src/platform/android/filesystem_saf.h b/src/platform/android/filesystem_saf.h index 85bc00e255..34002c08db 100644 --- a/src/platform/android/filesystem_saf.h +++ b/src/platform/android/filesystem_saf.h @@ -41,6 +41,7 @@ class SafFilesystem : public Filesystem { bool IsDirectory(std::string_view path, bool follow_symlinks) const override; bool Exists(std::string_view path) const override; int64_t GetFilesize(std::string_view path) const override; + bool vMakeDirectory(std::string_view path, bool follow_symlinks) const override; std::streambuf* CreateInputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; std::streambuf* CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; bool GetDirectoryContent(std::string_view path, std::vector& entries) const override; From 419906ff10c66e51227a774b230787349088ac83 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 17:45:42 +0200 Subject: [PATCH 15/20] Recursive Direction creating is now much smarter It goes downwards instead of upwards This prevents issues when path-like components are in the path that are not considered directories by the system. --- src/filesystem.cpp | 55 +++++++++++++++++++++++++---------------- src/filesystem_root.cpp | 2 +- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 810f0bdfba..5be20c0136 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -195,39 +195,52 @@ bool Filesystem::MakeDirectory(std::string_view path, bool follow_symlinks) cons cur_path += "/"; } - bool first = true; + // Create all the subpaths + // This will create e.g. for /a/b/c/d: + // ["/a", "/a/b", "/a/b/c", "/a/b/c/d"] + std::vector full_paths; for (const auto& comp : components) { if (comp.empty() || comp == ".") { continue; } cur_path = FileFinder::MakePath(cur_path, comp); + full_paths.push_back(cur_path); + } - if (first) { - // Do not check stuff that looks like drives, such as C:, ux0: or sd: - // Some systems do not consider them directories - first = false; - if (comp.back() == ':') { - continue; - } - } + if (full_paths.empty()) { + return true; + } -#if defined(__WIIU__) - if (cur_path == "fs:/vol" || cur_path == "/vol") { - // /vol is part of the path but checking for existance fails - continue; + // Do not check stuff that looks like drives, such as C:, ux0: or sd: + // Some systems do not consider them directories + if (full_paths[0].back() == ':') { + full_paths.erase(full_paths.begin()); + } + + // Traverse from the longest subpath and find the first one that exists + // e.g. assuming "/a/b" exists: + // First it checks "/a/b/c/d" (*), then "/a/b/c" (*), then ends at "/a/b" + std::vector to_create; + for (auto it = full_paths.rbegin(); it != full_paths.rend(); ++it) { + const std::string& p = *it; + + if (IsDirectory(p, follow_symlinks)) { + break; } -#endif - if (IsDirectory(cur_path, follow_symlinks)) { - continue; - } else if (Exists(cur_path)) { + if (Exists(p)) { // Exists but not a directory return false; - } else { - if (!vMakeDirectory(cur_path, follow_symlinks)) { - return false; - } + } + + to_create.push_back(p); + } + + // (*) These paths will be created hereZ + for (auto it = to_create.rbegin(); it != to_create.rend(); ++it) { + if (!vMakeDirectory(*it, follow_symlinks)) { + return false; } } diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index 0be7d24fbd..f5f28ab31d 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -105,7 +105,7 @@ bool RootFilesystem::GetDirectoryContent(std::string_view path, std::vector Date: Mon, 13 Jul 2026 14:20:02 -0400 Subject: [PATCH 16/20] Use the 64-bit stat function from libretro VFS v4 if available --- src/platform/libretro/filesystem_libretro.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index dc44c71575..25b9cbd88f 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -40,9 +40,15 @@ bool LibretroFilesystem::Exists(std::string_view filename) const { } int64_t LibretroFilesystem::GetFilesize(std::string_view path) const { - int32_t size; - int flags = vfs.iface->stat(ToString(path).c_str(), &size); - return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; + if (vfs.required_interface_version >= 4) { + int64_t size; + int flags = vfs.iface->stat_64(ToString(path).c_str(), &size); + return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; + } else { + int32_t size; + int flags = vfs.iface->stat(ToString(path).c_str(), &size); + return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; + } } // To prevent leaking of the file handle if an exception is thrown within CreateInputStreambuffer/CreateOutputStreambuffer From 8a0f591cfa9e949416a3f192b229d5203ad6e641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 13 Jul 2026 14:29:51 -0400 Subject: [PATCH 17/20] Fix the seek implementation in LibretroStreamBufIn --- src/platform/libretro/filesystem_libretro.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp index 25b9cbd88f..c243da9ed3 100644 --- a/src/platform/libretro/filesystem_libretro.cpp +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -98,7 +98,13 @@ class LibretroStreamBufIn : public std::streambuf { offset += static_cast(gptr() - egptr()); } int cdir = Filesystem_Stream::CppSeekdirToCSeekdir(dir); - auto res = LibretroFilesystem::vfs.iface->seek(handle, offset, cdir == SEEK_CUR ? RETRO_VFS_SEEK_POSITION_CURRENT : cdir == SEEK_END ? RETRO_VFS_SEEK_POSITION_END : RETRO_VFS_SEEK_POSITION_START); + if (LibretroFilesystem::vfs.iface->seek(handle, offset, cdir == SEEK_CUR ? RETRO_VFS_SEEK_POSITION_CURRENT : cdir == SEEK_END ? RETRO_VFS_SEEK_POSITION_END : RETRO_VFS_SEEK_POSITION_START) == -1) { + return -1; + } + auto res = LibretroFilesystem::vfs.iface->tell(handle); + if (res == -1) { + return -1; + } setg(buffer_start, buffer_end, buffer_end); return res; } From c4a4b2f70d67c72fecdc49c009da3a22cc011e82 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 29 Jul 2026 15:13:50 +0200 Subject: [PATCH 18/20] Refactor Startup when an Archive is loaded Code is now simpler and works for the namespaced paths libretro uses --- src/filefinder.cpp | 47 ++++++++++++++++++++- src/filefinder.h | 10 +++++ src/filesystem.cpp | 79 ++++++++++++------------------------ src/filesystem_root.cpp | 2 +- src/game_config.cpp | 1 - src/platform/libretro/ui.cpp | 2 + tests/filefinder.cpp | 25 ++++++++++++ 7 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 97e51e5aaf..1d0ff5c159 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -105,7 +105,9 @@ FilesystemView FileFinder::Save() { std::reverse(comps.begin(), comps.end()); std::string save_path = MakePath(lcf::MakeSpan(comps)); if (!parent.IsDirectory(save_path, true)) { - parent.MakeDirectory(save_path, true); + if (!parent.MakeDirectory(save_path, true)) { + Output::Debug("Save FS: MakeDirectory {} Failed", save_path); + } } redir = parent.Subtree(save_path); @@ -225,6 +227,49 @@ std::vector FileFinder::SplitPath(std::string_view path) { return Utils::Tokenize(path, f); } +std::vector FileFinder::SplitPathPrefixes(std::string_view path) { + // Check if the path contains a namespace and save it + std::string ns; + auto ns_pos = path.find("://"); + if (ns_pos != std::string::npos) { + ns = path.substr(0, ns_pos + 3); + path = path.substr(ns_pos + 3); + } + + auto components = FileFinder::SplitPath(path); + std::string cur_path; + if (StartsWith(path, "/")) { + cur_path += "/"; + } + + // Create all the subpaths + // This will create e.g. for /a/b/c/d: + // ["/a", "/a/b", "/a/b/c", "/a/b/c/d"] + std::vector full_paths; + for (const auto& comp : components) { + if (comp.empty() || comp == ".") { + continue; + } + + cur_path = FileFinder::MakePath(cur_path, comp); + full_paths.push_back(cur_path); + } + + if (EndsWith(path, "/")) { + full_paths.back() += "/"; + } + + // Prepend namespace to all paths + if (!ns.empty()) { + for (auto& s : full_paths) { + s = ns + s; + } + full_paths.insert(full_paths.begin(), ns); + } + + return full_paths; +} + std::pair FileFinder::GetPathAndFilename(std::string_view path) { if (path.empty()) { return {"", ""}; diff --git a/src/filefinder.h b/src/filefinder.h index effa98df64..aaaeea92aa 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -264,6 +264,16 @@ namespace FileFinder { */ std::vector SplitPath(std::string_view path); + /** + * Constructs a list of all the paths leading to the file. + * e.g. for /a/b/c/d it creates + * ["/a", "/a/b", "/a/b/c", "/a/b/c/d"] + * + * @param path Path to build prefixes from + * @return all path prefixes + */ + std::vector SplitPathPrefixes(std::string_view path); + /** * Splits a path into path and filename. * diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 5be20c0136..7505e611f7 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -91,62 +91,49 @@ FilesystemView Filesystem::Create(std::string_view path) const { // When the path doesn't exist check if the path contains a file that can // be handled by another filesystem if (!IsDirectory(path, true)) { - std::string dir_of_file; - std::string path_prefix; - std::vector components = FileFinder::SplitPath(path); - // TODO this should probably move to a static function in the FS classes // Search for the deepest directory - int i = 0; - for (const auto& comp : components) { + std::vector components = FileFinder::SplitPathPrefixes(path); + // Prepend empty path for the root directory if not present + if (components.empty() || components.front() != "") { + components.insert(components.begin(), ""); + } + + + size_t archive_idx = components.size(); + for (auto it = components.rbegin(); it != components.rend(); ++it) { + const std::string& p = *it; // Do not check stuff that looks like drives, such as C:, ux0: or sd: // Some systems do not consider them directories - if (i > 0 || (!comp.empty() && comp.back() != ':')) { - if (!IsDirectory(FileFinder::MakePath(dir_of_file, comp), true)) { + if (archive_idx > 1 || (!p.empty() && p.back() != ':')) { + if (IsDirectory(p, true)) { break; } + } else { + break; } - dir_of_file += comp + "/"; - ++i; - } - - if (!dir_of_file.empty()) { - dir_of_file.pop_back(); + --archive_idx; } // The next component must be a file // search for known file extensions and "do magic" - std::string internal_path; - bool handle_internal = false; - for (const auto& comp : lcf::MakeSpan(components).subspan(i)) { - if (handle_internal) { - internal_path += comp + "/"; - } else { - path_prefix += comp + "/"; - if (FileFinder::IsSupportedArchiveExtension(comp)) { - path_prefix.pop_back(); - handle_internal = true; - } - } - } + std::string archive_path = components[archive_idx - 1]; + std::string archive_name = FileFinder::GetPathAndFilename(components[archive_idx]).second; + std::string internal_path = FileFinder::GetPathInsidePath(components[archive_idx], components.back()); - if (!handle_internal) { + if (!FileFinder::IsSupportedArchiveExtension(archive_name)) { // No supported archive type found return {}; } - if (!internal_path.empty()) { - internal_path.pop_back(); - } - - std::shared_ptr filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + std::shared_ptr filesystem = std::make_shared(archive_name, Subtree(archive_path)); #if HAVE_LHASA if (!filesystem->IsValid()) { - filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + filesystem = std::make_shared(archive_name, Subtree(archive_path)); } #endif if (!filesystem->IsValid()) { - filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + filesystem = std::make_shared(archive_name, Subtree(archive_path)); } if (!filesystem->IsValid()) { return {}; @@ -189,24 +176,7 @@ bool Filesystem::MakeDirectory(std::string_view path, bool follow_symlinks) cons return true; } - auto components = FileFinder::SplitPath(path); - std::string cur_path; - if (StartsWith(path, "/")) { - cur_path += "/"; - } - - // Create all the subpaths - // This will create e.g. for /a/b/c/d: - // ["/a", "/a/b", "/a/b/c", "/a/b/c/d"] - std::vector full_paths; - for (const auto& comp : components) { - if (comp.empty() || comp == ".") { - continue; - } - - cur_path = FileFinder::MakePath(cur_path, comp); - full_paths.push_back(cur_path); - } + auto full_paths = FileFinder::SplitPathPrefixes(path); if (full_paths.empty()) { return true; @@ -237,9 +207,10 @@ bool Filesystem::MakeDirectory(std::string_view path, bool follow_symlinks) cons to_create.push_back(p); } - // (*) These paths will be created hereZ + // (*) These paths will be created here for (auto it = to_create.rbegin(); it != to_create.rend(); ++it) { if (!vMakeDirectory(*it, follow_symlinks)) { + Output::Debug("MakeDirectory {} failed", *it); return false; } } diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index f5f28ab31d..4eda56f82c 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -58,7 +58,7 @@ FilesystemView RootFilesystem::Create(std::string_view path) const { const auto& fs = FilesystemForPath(path); // Strip namespace from path except for libretro which expects them -#ifdef USE_LIBRETRO +#ifndef USE_LIBRETRO auto ns_pos = path.find("://"); if (ns_pos != std::string::npos) { path = path.substr(ns_pos + 3); diff --git a/src/game_config.cpp b/src/game_config.cpp index f30e81c36a..a5b493bf4f 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -347,7 +347,6 @@ Filesystem_Stream::OutputStream& Game_Config::GetLogFileOutput() { return noop_stream; } - // Make Directory not supported on Android, assume the path exists if (!FileFinder::Root().MakeDirectory(FileFinder::GetPathAndFilename(path).first, true)) { print_err(); return noop_stream; diff --git a/src/platform/libretro/ui.cpp b/src/platform/libretro/ui.cpp index d06ca1f311..f7ee4862dd 100644 --- a/src/platform/libretro/ui.cpp +++ b/src/platform/libretro/ui.cpp @@ -456,9 +456,11 @@ RETRO_API bool retro_load_game(const struct retro_game_info* game) { game_path = Utils::ReplaceAll(game_path, ".easyrpg#", ".easyrpg/"); game_path = FileFinder::MakeCanonical(game_path, 0); + log_cb(RETRO_LOG_INFO, "Loading Game %s\n", game_path.c_str()); auto fs = FileFinder::Root().Create(game_path); if (!fs) { std::tie(game_path, std::ignore) = FileFinder::GetPathAndFilename(game_path); + log_cb(RETRO_LOG_INFO, "Loading Game %s\n", game_path.c_str()); fs = FileFinder::Root().Create(game_path); if (!fs || !FileFinder::IsValidProject(fs)) { log_cb(RETRO_LOG_ERROR, "Unsupported game %s\n", game_path.c_str()); diff --git a/tests/filefinder.cpp b/tests/filefinder.cpp index 60cabf16d8..fe3bfa049d 100644 --- a/tests/filefinder.cpp +++ b/tests/filefinder.cpp @@ -114,4 +114,29 @@ TEST_CASE("GetPathAndFilename") { Player::escape_symbol = ""; } +TEST_CASE("SplitPathPrefixes") { + auto components = FileFinder::SplitPathPrefixes("folder/file"); + CHECK(components[0] == "folder"); + CHECK(components[1] == "folder/file"); + + components = FileFinder::SplitPathPrefixes("/folder/file"); + CHECK(components[0] == "/folder"); + CHECK(components[1] == "/folder/file"); + + components = FileFinder::SplitPathPrefixes("/folder/file/a/"); + CHECK(components[0] == "/folder"); + CHECK(components[1] == "/folder/file"); + CHECK(components[2] == "/folder/file/a/"); + + components = FileFinder::SplitPathPrefixes("c:/a/b"); + CHECK(components[0] == "c:"); + CHECK(components[1] == "c:/a"); + CHECK(components[2] == "c:/a/b"); + + components = FileFinder::SplitPathPrefixes("saf://content:%2F%2Fgames/File.zip"); + CHECK(components[0] == "saf://"); + CHECK(components[1] == "saf://content:%2F%2Fgames"); + CHECK(components[2] == "saf://content:%2F%2Fgames/File.zip"); +} + TEST_SUITE_END(); From 9a11b4b5235f94720716c9e98673dc04e14bff24 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 29 Jul 2026 15:18:13 +0200 Subject: [PATCH 19/20] Reset Save Directory when a new game is loaded Fix #3549 --- src/player.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/player.cpp b/src/player.cpp index 1ff38f8c6c..a43104f739 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -457,6 +457,8 @@ Game_Config Player::ParseCommandLine() { bool battletest_handled = false; + FileFinder::SetSaveFilesystem({}); + cp.Rewind(); if (!cp.Done()) { // BattleTest argument handling in a RPG_RT compatible way is very ugly because the arguments do not follow From 4f4b6b7f942d46e948a83e79ea6e46953b37b981 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 29 Jul 2026 16:44:42 +0200 Subject: [PATCH 20/20] Android: Fixes to make the new Create function also work with our App --- .../org_easyrpg_player_game_browser.cpp | 2 +- .../org/easyrpg/player/game_browser/Game.java | 8 +++++++- .../player/game_browser/GameScanner.java | 8 +++++++- src/filesystem.cpp | 17 ++++++++--------- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index 87fd5382d2..c6404438e8 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -484,7 +484,7 @@ Java_org_easyrpg_player_settings_SettingsFontActivity_DrawText(JNIEnv *env, jcla jbyte* buffer_raw = env->GetByteArrayElements(buffer_array, 0); Bitmap::SetFormat(Bitmap::ChooseFormat(format_R8G8B8A8_a().format())); - auto sys = Cache::System(CACHE_DEFAULT_BITMAP); + auto sys = Cache::System(CACHE_DEFAULT_BITMAP, false); BitmapRef draw_area = Bitmap::Create(reinterpret_cast(buffer_raw), width, height, 0, format_R8G8B8A8_a().format()); draw_area->Fill(Color(0, 0, 0, 255)); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java index 800501bc6c..19ef7d747b 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java @@ -219,7 +219,13 @@ public static Game fromCacheEntry(Context context, String cache) { } String savePath = entries[0]; - DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[1])); + DocumentFile gameFolder = null; + try { + gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[1])); + } catch (IllegalArgumentException e) { + return null; + } + if (gameFolder == null) { return null; } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java index 0db5e37ec4..6926b47536 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java @@ -169,7 +169,13 @@ private void scanRootFolder(Activity activity, Uri folderURI) { myTextView.setText(String.format("%s (%d/%d)", name, j + 1, names.size())); }); - Game[] candidates = findGames(fileURIs.get(i).toString(), names.get(i)); + String fileURI = fileURIs.get(i).toString(); + int encoded_slash_pos = fileURI.lastIndexOf("%2F"); + // Encode everything from the last %2F so our native code works properly + String toDecode = fileURI.substring(encoded_slash_pos); + toDecode = Uri.decode(toDecode); + + Game[] candidates = findGames(fileURI.substring(0, encoded_slash_pos) + toDecode, names.get(i)); if (candidates == null) { continue; diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 7505e611f7..1a2c50fdb5 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -91,7 +91,6 @@ FilesystemView Filesystem::Create(std::string_view path) const { // When the path doesn't exist check if the path contains a file that can // be handled by another filesystem if (!IsDirectory(path, true)) { - // Search for the deepest directory std::vector components = FileFinder::SplitPathPrefixes(path); // Prepend empty path for the root directory if not present @@ -99,7 +98,6 @@ FilesystemView Filesystem::Create(std::string_view path) const { components.insert(components.begin(), ""); } - size_t archive_idx = components.size(); for (auto it = components.rbegin(); it != components.rend(); ++it) { const std::string& p = *it; @@ -117,23 +115,24 @@ FilesystemView Filesystem::Create(std::string_view path) const { // The next component must be a file // search for known file extensions and "do magic" - std::string archive_path = components[archive_idx - 1]; - std::string archive_name = FileFinder::GetPathAndFilename(components[archive_idx]).second; - std::string internal_path = FileFinder::GetPathInsidePath(components[archive_idx], components.back()); + std::string archive_full_path = components[archive_idx]; + std::string archive_parent_path = components[archive_idx - 1]; + std::string archive_name = FileFinder::GetPathAndFilename(archive_full_path).second; + std::string internal_path = FileFinder::GetPathInsidePath(archive_full_path, components.back()); - if (!FileFinder::IsSupportedArchiveExtension(archive_name)) { + if (!IsFile(archive_full_path) || !FileFinder::IsSupportedArchiveExtension(archive_name)) { // No supported archive type found return {}; } - std::shared_ptr filesystem = std::make_shared(archive_name, Subtree(archive_path)); + std::shared_ptr filesystem = std::make_shared(archive_name, Subtree(archive_parent_path)); #if HAVE_LHASA if (!filesystem->IsValid()) { - filesystem = std::make_shared(archive_name, Subtree(archive_path)); + filesystem = std::make_shared(archive_name, Subtree(archive_parent_path)); } #endif if (!filesystem->IsValid()) { - filesystem = std::make_shared(archive_name, Subtree(archive_path)); + filesystem = std::make_shared(archive_name, Subtree(archive_parent_path)); } if (!filesystem->IsValid()) { return {};