diff --git a/CMakeLists.txt b/CMakeLists.txt index 88c93137a9..86b309cc2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,8 @@ add_library(${PROJECT_NAME} OBJECT src/fileext_guesser.h src/filesystem.cpp src/filesystem.h + src/filesystem_drive.cpp + src/filesystem_drive.h src/filesystem_lzh.cpp src/filesystem_lzh.h src/filesystem_native.cpp diff --git a/Makefile.am b/Makefile.am index c05af3518a..0390b833b2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -103,6 +103,8 @@ libeasyrpg_player_a_SOURCES = \ src/fileext_guesser.h \ src/filesystem.cpp \ src/filesystem.h \ + src/filesystem_drive.cpp \ + src/filesystem_drive.h \ src/filesystem_lzh.cpp \ src/filesystem_lzh.h \ src/filesystem_native.cpp \ diff --git a/src/directory_tree.h b/src/directory_tree.h index 6232f5814e..71f72ba50f 100644 --- a/src/directory_tree.h +++ b/src/directory_tree.h @@ -43,6 +43,8 @@ class DirectoryTree { Regular, /** Directory */ Directory, + /** A virtual directory that will access a different virtual filesystem */ + Filesystem, /** Anything of no interest such as block devices */ Other }; @@ -53,8 +55,15 @@ class DirectoryTree { std::string name; /** File type */ FileType type; + /** Human readable name shown in the Game Browser (if different to the filename) */ + std::string human_name; Entry(std::string name, FileType type) : name(std::move(name)), type(type) {} + Entry(std::string name, FileType type, std::string human_name) : name(std::move(name)), type(type), human_name(std::move(human_name)) {} + + StringView GetReadableName() const { + return human_name.empty() ? name : human_name; + } }; /** Argument struct for more complex find operations */ diff --git a/src/filefinder.cpp b/src/filefinder.cpp index cc6b9004cc..bc08b96770 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -143,7 +143,7 @@ FilesystemView FileFinder::Root() { root_fs = std::make_unique(); } - return root_fs->Subtree(""); + return *root_fs; } std::string FileFinder::MakePath(StringView dir, StringView name) { diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 132558f60c..1649512b5f 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -87,9 +87,13 @@ void Filesystem::ClearCache(StringView path) const { FilesystemView Filesystem::Create(StringView path) const { // Determine the proper file system to use - // When the path doesn't exist check if the path contains a file that can - // be handled by another filesystem - if (!IsDirectory(path, true)) { + if (IsFilesystemNode(path)) { + // The support for "mounted" virtual filesystems is very limited and the only + // use right now is to delegate from DriveFilesystem to NativeFilesystem. + return CreateFromNode(path); + } else if (!IsDirectory(path, true)) { + // When the path doesn't exist check if the path contains a file that can + // be handled by another filesystem std::string dir_of_file; std::string path_prefix; std::vector components = FileFinder::SplitPath(path); @@ -175,10 +179,18 @@ FilesystemView Filesystem::Subtree(std::string sub_path) const { return FilesystemView(shared_from_this(), sub_path); } +bool Filesystem::IsFilesystemNode(StringView) const { + return false; +} + bool Filesystem::MakeDirectory(StringView, bool) const { return false; } +FilesystemView Filesystem::CreateFromNode(StringView) const { + return FilesystemView(); +} + bool Filesystem::IsValid() const { // FIXME: better way to do this? return Exists(""); @@ -307,6 +319,11 @@ bool FilesystemView::IsDirectory(StringView path, bool follow_symlinks) const { return fs->IsDirectory(MakePath(path), follow_symlinks); } +bool FilesystemView::IsFilesystemNode(StringView path) const { + assert(fs); + return fs->IsFilesystemNode(MakePath(path)); +} + bool FilesystemView::Exists(StringView path) const { assert(fs); return fs->Exists(MakePath(path)); @@ -372,6 +389,11 @@ bool FilesystemView::MakeDirectory(StringView dir, bool follow_symlinks) const { return fs->MakeDirectory(MakePath(dir), follow_symlinks); } +FilesystemView FilesystemView::CreateFromNode(StringView path) const { + assert(fs); + return fs->CreateFromNode(MakePath(path)); +} + bool FilesystemView::IsFeatureSupported(Filesystem::Feature f) const { assert(fs); return fs->IsFeatureSupported(f); @@ -382,6 +404,24 @@ FilesystemView FilesystemView::Subtree(StringView sub_path) const { return FilesystemView(fs, MakePath(sub_path)); } +bool FilesystemView::CanGoUp() const { + return static_cast(GoUp()); +} + +FilesystemView FilesystemView::GoUp() const { + if (GetSubPath().empty() || GetSubPath() == "/") { + return fs->GetParent(); + } + + auto [path, file] = FileFinder::GetPathAndFilename(GetSubPath()); + + if (path == GetSubPath()) { + return fs->GetParent(); + } + + return FilesystemView(fs, path); +} + std::string FilesystemView::Describe() const { assert(fs); if (GetSubPath().empty()) { diff --git a/src/filesystem.h b/src/filesystem.h index 02936dcdee..096deca019 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -217,9 +217,11 @@ class Filesystem : public std::enable_shared_from_this { /** @{ */ virtual bool IsFile(StringView path) const = 0; virtual bool IsDirectory(StringView path, bool follow_symlinks) const = 0; + virtual bool IsFilesystemNode(StringView path) const; virtual bool Exists(StringView path) const = 0; virtual int64_t GetFilesize(StringView path) const = 0; virtual bool MakeDirectory(StringView dir, bool follow_symlinks) const; + virtual FilesystemView CreateFromNode(StringView path) const; virtual bool IsFeatureSupported(Feature f) const; virtual std::string Describe() const = 0; /** @} */ @@ -369,6 +371,12 @@ class FilesystemView { */ bool IsDirectory(StringView path, bool follow_symlinks) const; + /** + * @param path Path to check + * @return True when path is pointing to a virtual filesystem + */ + bool IsFilesystemNode(StringView path) const; + /** * @param path Path to check * @return True when a file exists at the path @@ -461,6 +469,14 @@ class FilesystemView { */ bool MakeDirectory(StringView dir, bool follow_symlinks) const; + /** + * Create a filesystem view from the passed in path. + * The path must point to a virtual filesystem entry (type Filesystem). + * @param path Path to create filesystem from + * @return view pointing at the new fs + */ + FilesystemView CreateFromNode(StringView path) const; + /** * @param f Filesystem feature to check * @return true when the feature is supported. @@ -475,6 +491,19 @@ class FilesystemView { */ FilesystemView Subtree(StringView sub_path) const; + /** + * @return Whether it is possible to go up from the current view + */ + bool CanGoUp() const; + + /** + * From the current view goes up by one. + * Returns an invalid view when going up is not possible (no parent). + * + * @return View that is rooted at the parent. + */ + FilesystemView GoUp() const; + /** @return human readable representation of this filesystem for debug purposes */ std::string Describe() const; diff --git a/src/filesystem_drive.cpp b/src/filesystem_drive.cpp new file mode 100644 index 0000000000..67aced70f4 --- /dev/null +++ b/src/filesystem_drive.cpp @@ -0,0 +1,110 @@ +/* + * 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_drive.h" +#include "filefinder.h" +#include "output.h" + +#include + +#ifdef _WIN32 +# include +# include +#endif + +DriveFilesystem::DriveFilesystem() : Filesystem("", FilesystemView()) { +#ifdef _WIN32 + std::wstring volume = L"A:\\"; + + DWORD logical_drives = GetLogicalDrives(); + for (int i = 0; i < 26; i++) { + if ((logical_drives & (1 << i)) > 0) { + DirectoryTree::Entry entry = { Utils::FromWideString(volume), DirectoryTree::FileType::Filesystem }; + + wchar_t volume_name[MAX_PATH]; + if (GetVolumeInformation(volume.c_str(), volume_name, MAX_PATH, nullptr, nullptr, nullptr, nullptr, 0) != 0) { + entry.human_name = fmt::format("{} ({})", Utils::FromWideString(volume), Utils::FromWideString(volume_name)); + } + + drives.push_back(entry); + } + volume[0]++; // Increment drive letter + } +#endif +} + +bool DriveFilesystem::HasDrives() const { + return !drives.empty(); +} + +bool DriveFilesystem::IsFile(StringView path) const { + (void)path; + return false; +} + +bool DriveFilesystem::IsDirectory(StringView path, bool) const { + return path.empty(); +} + +bool DriveFilesystem::IsFilesystemNode(StringView path) const { + for (const auto& drive: drives) { + if (drive.name == path) { + return true; + } +#ifdef _WIN32 + if (drive.name == Utils::ReplaceAll(ToString(path), "/", "\\")) { + return true; + } +#endif + } + + return false; +} + +bool DriveFilesystem::Exists(StringView path) const { + return IsDirectory(path, false) || IsFilesystemNode(path); +} + +int64_t DriveFilesystem::GetFilesize(StringView path) const { + (void)path; + return 0; +} + +FilesystemView DriveFilesystem::CreateFromNode(StringView path) const { + if (!IsFilesystemNode(path)) { + return {}; + } + + return FileFinder::Root().Create(path); +} + +std::streambuf* DriveFilesystem::CreateInputStreambuffer(StringView path, std::ios_base::openmode mode) const { + return nullptr; +} + +bool DriveFilesystem::GetDirectoryContent(StringView path, std::vector& tree) const { + if (!path.empty()) { + return false; + } + + tree = drives; + return true; +} + +std::string DriveFilesystem::Describe() const { + return "[Drive]"; +} diff --git a/src/filesystem_drive.h b/src/filesystem_drive.h new file mode 100644 index 0000000000..83985f9ee9 --- /dev/null +++ b/src/filesystem_drive.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_DRIVE_H +#define EP_FILESYSTEM_DRIVE_H + +#include "filesystem.h" + +/** + * A virtual filesystem that lists e.g. drive letters on Windows + */ +class DriveFilesystem : public Filesystem { +public: + /** + * Initializes a OS Filesystem on the given os path + */ + explicit DriveFilesystem(); + + /** @return Whether the current target platform has drive letters to list */ + bool HasDrives() const; + +protected: + /** + * Implementation of abstract methods + */ + /** @{ */ + bool IsFile(StringView path) const override; + bool IsDirectory(StringView path, bool follow_symlinks) const override; + bool IsFilesystemNode(StringView path) const override; + bool Exists(StringView path) const override; + int64_t GetFilesize(StringView path) const override; + FilesystemView CreateFromNode(StringView path) const override; + std::streambuf* CreateInputStreambuffer(StringView path, std::ios_base::openmode mode) const override; + bool GetDirectoryContent(StringView path, std::vector& entries) const override; + std::string Describe() const override; + /** @} */ + +private: + std::vector drives; +}; + +#endif diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index 270c6efa7f..4270cfcd47 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -16,6 +16,7 @@ */ #include "filesystem_root.h" +#include "filesystem_drive.h" #include "output.h" #if defined(__ANDROID__) && !defined(USE_LIBRETRO) @@ -28,12 +29,20 @@ constexpr const StringView root_ns = "root://"; RootFilesystem::RootFilesystem() : Filesystem("", FilesystemView()) { // Add platform specific namespaces here #if defined(__ANDROID__) && !defined(USE_LIBRETRO) - fs_list.push_back(std::make_pair("apk", std::make_unique())); - fs_list.push_back(std::make_pair("content", std::make_unique("", FilesystemView()))); + fs_list.push_back(std::make_pair("apk", std::make_shared())); + fs_list.push_back(std::make_pair("content", std::make_shared("", FilesystemView()))); #endif + // Support for drive letters on e.g. Windows (and similiar concepts on other platforms) + auto drive_fs = std::make_shared(); + FilesystemView drive_view; + if (drive_fs->HasDrives()) { + drive_view = *drive_fs; + fs_list.push_back(std::make_pair("drive", drive_fs)); + } + // 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()))); + fs_list.push_back(std::make_pair("file", std::make_shared("", drive_view))); assert(fs_list.back().first == "file" && "File namespace must be last!"); } diff --git a/src/main_data.cpp b/src/main_data.cpp index 86d4a8cc31..8dd1ef6bb9 100644 --- a/src/main_data.cpp +++ b/src/main_data.cpp @@ -41,9 +41,12 @@ #include "system.h" #include "output.h" -#ifndef _WIN32 +#ifdef _WIN32 +# include +#else # include #endif + #if defined(USE_SDL) && defined(__ANDROID__) # include # include @@ -89,22 +92,31 @@ void Main_Data::Init() { // Set to current directory project_path = ""; -#if defined(PLAYER_AMIGA) - // Working directory not correctly handled - char working_dir[256]; - getcwd(working_dir, 255); - project_path = std::string(working_dir); -#elif defined(__APPLE__) && TARGET_OS_OSX +#ifdef _WIN32 + wchar_t working_dir[MAX_PATH]; + if (GetCurrentDirectory(MAX_PATH, working_dir) != 0) { + project_path = Utils::FromWideString(working_dir); + } +#else + +# ifndef PATH_MAX +# define PATH_MAX 256 +# endif + char working_dir[PATH_MAX]; + if (getcwd(working_dir, sizeof(working_dir))) { + project_path = std::string(working_dir); + } + +# if defined(__APPLE__) && TARGET_OS_OSX // Apple Finder does not set the working directory // It points to HOME instead. When it is HOME change it to // the application directory instead char* home = getenv("HOME"); - char current_dir[PATH_MAX] = { 0 }; - getcwd(current_dir, sizeof(current_dir)); - if (strcmp(current_dir, "/") == 0 || strcmp(current_dir, home) == 0) { + if (strcmp(working_dir, "/") == 0 || strcmp(working_dir, home) == 0) { project_path = MacOSUtils::GetBundleDir(); } +# endif #endif } } diff --git a/src/platform.h b/src/platform.h index a285822c7f..dacba8047c 100644 --- a/src/platform.h +++ b/src/platform.h @@ -157,6 +157,8 @@ namespace Platform { inline Directory::operator bool() const noexcept { #ifdef __vita__ return dir_handle >= 0; +#elif defined(_WIN32) + return dir_handle != INVALID_HANDLE_VALUE; #else return dir_handle != nullptr; #endif diff --git a/src/scene_gamebrowser.cpp b/src/scene_gamebrowser.cpp index 5a0b700569..9c0f889b75 100644 --- a/src/scene_gamebrowser.cpp +++ b/src/scene_gamebrowser.cpp @@ -99,12 +99,16 @@ void Scene_GameBrowser::CreateWindows() { command_window->SetIndex(0); gamelist_window = std::make_unique(0, 64, Player::screen_width, Player::screen_height - 64); - gamelist_window->Refresh(stack.back().filesystem, false); + gamelist_window->Refresh(stack.back().filesystem, stack.back().filesystem.CanGoUp()); - if (stack.size() == 1 && !gamelist_window->HasValidEntry()) { + if (stack.size() == 1 && !stack.back().filesystem.CanGoUp() && !gamelist_window->HasValidEntry()) { command_window->DisableItem(0); } +#ifdef EMSCRIPTEN + command_window->DisableItem(0); +#endif + help_window = std::make_unique(0, 0, Player::screen_width, 32); help_window->SetText("EasyRPG Player - RPG Maker 2000/2003 interpreter"); @@ -140,7 +144,7 @@ void Scene_GameBrowser::UpdateCommand() { switch (menu_index) { case GameList: - if (stack.size() == 1 && !gamelist_window->HasValidEntry()) { + if (!command_window->IsItemEnabled(0)) { return; } command_window->SetActive(false); @@ -177,20 +181,24 @@ void Scene_GameBrowser::UpdateGameListSelection() { } void Scene_GameBrowser::BootGame() { - if (stack.size() > 1 && gamelist_window->GetIndex() == 0) { + if (stack.back().filesystem.CanGoUp() && gamelist_window->GetIndex() == 0) { // ".." -> Go one level up int index = stack.back().index; - stack.pop_back(); - gamelist_window->Refresh(stack.back().filesystem, stack.size() > 1); + + if (stack.size() == 1) { + stack.back() = {stack.back().filesystem.GoUp(), 0}; + } else { + stack.pop_back(); + } + + gamelist_window->Refresh(stack.back().filesystem, stack.back().filesystem.CanGoUp()); gamelist_window->SetIndex(index); load_window->SetVisible(false); game_loading = false; return; } - FilesystemView fs; - std::string entry; - std::tie(fs, entry) = gamelist_window->GetGameFilesystem(); + FilesystemView fs = gamelist_window->GetGameFilesystem(); if (!fs) { Output::Warning("The selected file or directory cannot be opened"); diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index bf6b58fed0..3daee9ef13 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -55,21 +55,21 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) } if (dir.second.type == DirectoryTree::FileType::Regular) { if (FileFinder::IsSupportedArchiveExtension(dir.second.name)) { - game_directories.emplace_back(dir.second.name); + game_directories.emplace_back(dir.second); } - } else if (dir.second.type == DirectoryTree::FileType::Directory) { - game_directories.emplace_back(dir.second.name); + } else if (dir.second.type == DirectoryTree::FileType::Directory || dir.second.type == DirectoryTree::FileType::Filesystem) { + game_directories.emplace_back(dir.second); } } // Sort game list in place std::sort(game_directories.begin(), game_directories.end(), - [](const std::string& s, const std::string& s2) { - return strcmp(Utils::LowerCase(s).c_str(), Utils::LowerCase(s2).c_str()) <= 0; + [](DirectoryTree::Entry& s, DirectoryTree::Entry& s2) { + return strcmp(Utils::LowerCase(s.GetReadableName()).c_str(), Utils::LowerCase(s2.GetReadableName()).c_str()) <= 0; }); if (show_dotdot) { - game_directories.insert(game_directories.begin(), ".."); + game_directories.insert(game_directories.begin(), { "..", DirectoryTree::FileType::Directory }); } if (HasValidEntry()) { @@ -84,6 +84,10 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) } } else { +#ifdef EMSCRIPTEN + show_dotdot = false; +#endif + item_max = 1; SetContents(Bitmap::Create(width - 16, height - 16)); @@ -102,15 +106,16 @@ void Window_GameList::DrawItem(int index) { Rect rect = GetItemRect(index); contents->ClearRect(rect); - std::string text; - - if (HasValidEntry()) { - text = game_directories[index]; - } - - contents->TextDraw(rect.x, rect.y, Font::ColorDefault, game_directories[index]); + StringView text = game_directories[index].GetReadableName(); + contents->TextDraw(rect.x, rect.y, Font::ColorDefault, text); } +#ifdef HAVE_LHASA +#define LZH_STR "/LZH" +#else +#define LZH_STR "" +#endif + void Window_GameList::DrawErrorText(bool show_dotdot) { std::vector error_msg = { #ifdef EMSCRIPTEN @@ -128,7 +133,7 @@ void Window_GameList::DrawErrorText(bool show_dotdot) { "with RPG Maker 2000 and RPG Maker 2003.", "", "These games have an RPG_RT.ldb and they can be", - "extracted or in ZIP archives.", + "extracted or in ZIP" LZH_STR " archives.", "", "Newer engines such as RPG Maker XP, VX, MV and MZ", "are not supported." @@ -154,6 +159,6 @@ bool Window_GameList::HasValidEntry() { return game_directories.size() > minval; } -std::pair Window_GameList::GetGameFilesystem() const { - return { base_fs.Create(game_directories[GetIndex()]), game_directories[GetIndex()] }; +FilesystemView Window_GameList::GetGameFilesystem() const { + return base_fs.Create(game_directories[GetIndex()].name); } diff --git a/src/window_gamelist.h b/src/window_gamelist.h index a3809c6f86..4e27c47595 100644 --- a/src/window_gamelist.h +++ b/src/window_gamelist.h @@ -55,13 +55,13 @@ class Window_GameList : public Window_Selectable { bool HasValidEntry(); /** - * @return filesystem and entry name of the selected game + * @return filesystem of the selected game */ - std::pair GetGameFilesystem() const; + FilesystemView GetGameFilesystem() const; private: FilesystemView base_fs; - std::vector game_directories; + std::vector game_directories; bool show_dotdot = false; }; diff --git a/tests/filefinder.cpp b/tests/filefinder.cpp index 60cabf16d8..b40f931c04 100644 --- a/tests/filefinder.cpp +++ b/tests/filefinder.cpp @@ -8,8 +8,6 @@ TEST_SUITE_BEGIN("FileFinder"); TEST_CASE("IsRPG2kProject") { - Main_Data::Init(); - Player::escape_symbol = "\\"; auto fs = FileFinder::Root().Subtree(EP_TEST_PATH "/game"); @@ -19,8 +17,6 @@ TEST_CASE("IsRPG2kProject") { } TEST_CASE("IsNotRPG2kProject") { - Main_Data::Init(); - auto fs = FileFinder::Root().Subtree(EP_TEST_PATH "/notagame"); CHECK(!FileFinder::IsRPG2kProject(fs)); } diff --git a/tests/output.cpp b/tests/output.cpp index 0305c06768..33bf759247 100644 --- a/tests/output.cpp +++ b/tests/output.cpp @@ -7,7 +7,6 @@ TEST_SUITE_BEGIN("Output"); TEST_CASE("Message Output") { Graphics::Init(); - Main_Data::Init(); Output::Debug("Test {}", "debg"); Output::Warning("Test {}", "test"); Output::Info("Test {}", "info");