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/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/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/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 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 b292328820..1a2c50fdb5 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -91,62 +91,48 @@ 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_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 (!handle_internal) { + if (!IsFile(archive_full_path) || !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_parent_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_parent_path)); } #endif if (!filesystem->IsValid()) { - filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + filesystem = std::make_shared(archive_name, Subtree(archive_parent_path)); } if (!filesystem->IsValid()) { return {}; @@ -184,7 +170,54 @@ 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 full_paths = FileFinder::SplitPathPrefixes(path); + + if (full_paths.empty()) { + return true; + } + + // 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; + } + + if (Exists(p)) { + // Exists but not a directory + return false; + } + + to_create.push_back(p); + } + + // (*) 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; + } + } + + 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 4cb334590c..4eda56f82c 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()))); @@ -49,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 +#ifndef USE_LIBRETRO auto ns_pos = path.find("://"); if (ns_pos != std::string::npos) { path = path.substr(ns_pos + 3); } +#endif return fs.Create(path); } @@ -94,8 +104,8 @@ bool RootFilesystem::GetDirectoryContent(std::string_view path, std::vector= 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/filesystem_root.h b/src/filesystem_root.h index 65f3cf6ef0..9d8f299d79 100644 --- a/src/filesystem_root.h +++ b/src/filesystem_root.h @@ -67,7 +67,7 @@ class RootFilesystem : 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; std::string Describe() const override; /** @} */ diff --git a/src/game_config.cpp b/src/game_config.cpp index 9d530ccbf6..a5b493bf4f 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -347,13 +347,10 @@ 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/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.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/android/filesystem_saf.cpp b/src/platform/android/filesystem_saf.cpp index bd89e28f66..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) { @@ -117,7 +131,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/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; diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp new file mode 100644 index 0000000000..c243da9ed3 --- /dev/null +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -0,0 +1,251 @@ +/* + * 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 { + 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 +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) { + setg(buffer_start, buffer_start, buffer_start); + } + + ~LibretroStreamBufIn() override { + LibretroFilesystem::vfs.iface->close(handle); + } + + int underflow() override { + int64_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); + 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; + } + + std::streambuf::pos_type seekpos(std::streambuf::pos_type pos, std::ios_base::openmode mode) override { + return seekoff(pos, std::ios_base::beg, mode); + } + +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); + if (handle == nullptr) { + return nullptr; + } + LibretroFileGuard guard(handle); + LibretroStreamBufIn* stream = new LibretroStreamBufIn(handle); + guard.forget(); + return stream; +} + +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); + int64_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; + } + int64_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 mode) const { + struct retro_vfs_file_handle* handle; + if ((mode & std::ios_base::app) == std::ios_base::app && Exists(path)) { + 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; + } + } else { + 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); + LibretroStreamBufOut* stream = new LibretroStreamBufOut(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) noexcept : 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::vMakeDirectory(std::string_view path, bool) const { + return vfs.iface->mkdir(ToString(path).c_str()) != -1; +} + +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..08693634b4 --- /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 vMakeDirectory(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..f7ee4862dd 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,12 @@ RETRO_API void retro_set_environment(retro_environment_t cb) { { nullptr, nullptr } }; cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); + + 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; + } } RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb) { @@ -449,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/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 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();