From 8a18a8c12cbe08fba874de3543c8e424ea978aed Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:39:24 +0200 Subject: [PATCH 01/26] Android: Hardcode the read only strings Reduces confusion in Weblate due to the Read only strings. --- .../player/game_browser/GameBrowserHelper.java | 14 +++++++------- builds/android/app/src/main/res/values/strings.xml | 7 ------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index e20b518379..322e374518 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -242,31 +242,31 @@ public static void showErrorMessage(Context context, SafError error) { break; case BAD_CONTENT_PROVIDER_CREATE: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_create); + errorMsg += "File creation failed."; break; case BAD_CONTENT_PROVIDER_READ: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_read); + errorMsg += "Read operation failed."; break; case BAD_CONTENT_PROVIDER_WRITE: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_write); + errorMsg += "Write operation failed."; break; case BAD_CONTENT_PROVIDER_DELETE: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_delete); + errorMsg += "File deletion failed."; break; case BAD_CONTENT_PROVIDER_FILENAME_IGNORED: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_filename_ignored); + errorMsg += "Provided filename ignored."; break; case BAD_CONTENT_PROVIDER_BASE_FOLDER_NOT_FOUND: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_base_folder_not_found); + errorMsg += "Selected folder not found."; break; case BAD_CONTENT_PROVIDER_FILE_ACCESS: errorMsg = context.getString(R.string.error_saf_bad_content_provider); - errorMsg += context.getString(R.string.error_saf_bad_content_provider_file_access); + errorMsg += "A file was successfully created but cannot be accessed."; break; case FOLDER_NOT_ALMOST_EMPTY: errorMsg = context.getString(R.string.error_saf_folder_not_empty); diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index e12a3a2135..fc7ba497ed 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -47,13 +47,6 @@ The Download folder cannot be used as the EasyRPG folder. The selected folder contains other files and cannot be used as the EasyRPG folder. When you remove all files inside the folder you can use it as the EasyRPG folder. The selected folder cannot be used as the EasyRPG folder. This problem can occur for various reasons, for example if you have selected the root directory or a storage location in the cloud.\n\n - File creation failed. - Read operation failed. - Write operation failed. - File deletion failed. - Provided filename ignored. - Selected folder not found. - A file was successfully created but cannot be accessed. Toggle virtual buttons From f24639f44f92b0adae84524d6660ed7874588338 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:40:40 +0200 Subject: [PATCH 02/26] Android: Remove FastForward button by default Can be added in the Layout editor. --- .../java/org/easyrpg/player/button_mapping/InputLayout.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/button_mapping/InputLayout.java b/builds/android/app/src/main/java/org/easyrpg/player/button_mapping/InputLayout.java index 15b4412d11..d6fcff175f 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/button_mapping/InputLayout.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/button_mapping/InputLayout.java @@ -79,7 +79,6 @@ public String toStringForSave(Activity activity) { private static LinkedList getDefaultHorizontalButtonList(Activity activity) { LinkedList l = new LinkedList<>(); l.add(new MenuButton(activity, 0.01, 0.01, 90)); - l.add(VirtualButton.Create(activity, VirtualButton.KEY_FAST_FORWARD, 0.9, 0.01, 90)); l.add(new VirtualCross(activity, 0.01, 0.4, 100)); l.add(VirtualButton.Create(activity, VirtualButton.ENTER, 0.80, 0.55, 100)); l.add(VirtualButton.Create(activity, VirtualButton.CANCEL, 0.90, 0.45, 100)); @@ -90,7 +89,6 @@ private static LinkedList getDefaultHorizontalButtonList(Activity private static LinkedList getDefaultVerticalButtonList(Activity activity) { LinkedList l = new LinkedList<>(); l.add(new MenuButton(activity, 0.01, 0.5, 90)); - l.add(VirtualButton.Create(activity, VirtualButton.KEY_FAST_FORWARD, 0.70, 0.5, 90)); l.add(new VirtualCross(activity, 0.05, 0.65, 100)); l.add(VirtualButton.Create(activity, VirtualButton.ENTER, 0.60, 0.75, 100)); l.add(VirtualButton.Create(activity, VirtualButton.CANCEL, 0.70, 0.65, 100)); From c76ea074c26da12bb01c44b280d461c2a519c560 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:41:59 +0200 Subject: [PATCH 03/26] FileFinder: Add a helper function to recursively find a game To be used by the Android GameBrowser --- src/filefinder.cpp | 29 +++++++++++++++++++++++++++++ src/filefinder.h | 9 +++++++++ src/utils.h | 1 - 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index e78ae58053..31fbad16db 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -35,6 +35,7 @@ #include "system.h" #include "options.h" #include "utils.h" +#include "directory_tree.h" #include "filefinder.h" #include "filefinder_rtp.h" #include "filesystem.h" @@ -531,3 +532,31 @@ void FileFinder::DumpFilesystem(FilesystemView fs) { cur_fs = cur_fs.GetOwner().GetParent(); } } + +FilesystemView FileFinder::FindGameRecursive(FilesystemView fs, int recursion_limit) { + if (!fs || recursion_limit == 0) { + return {}; + } + + if (IsValidProject(fs)) { + return fs; + } + + auto entries = fs.ListDirectory(); + + for (auto& [name_lower, entry]: *entries) { + if (entry.type == DirectoryTree::FileType::Directory) { + auto fs_ret = FindGameRecursive(fs.Subtree(entry.name), recursion_limit - 1); + if (fs_ret) { + return fs_ret; + } + } else if (entry.type == DirectoryTree::FileType::Regular && IsSupportedArchiveExtension(entry.name)) { + auto fs_ret = FindGameRecursive(fs.Create(entry.name)); + if (fs_ret) { + return fs_ret; + } + } + } + + return {}; +} diff --git a/src/filefinder.h b/src/filefinder.h index 3bd774afcf..f747355eb3 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -353,6 +353,15 @@ namespace FileFinder { * @param fs Filesystem to use */ void DumpFilesystem(FilesystemView fs); + + /** + * Searches recursively for a game directory. + * + * @param fs Filesystem where the search starts + * @param recursion_limit Recursion depth + * @return View of the game directory when found + */ + FilesystemView FindGameRecursive(FilesystemView fs, int recursion_limit = 3); } // namespace FileFinder template diff --git a/src/utils.h b/src/utils.h index 53bd82349a..0140213306 100644 --- a/src/utils.h +++ b/src/utils.h @@ -25,7 +25,6 @@ #include #include #include -#include "system.h" #include "string_view.h" #include "span.h" From af01610912338456209549e7c636584b5f4be2d6 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:42:21 +0200 Subject: [PATCH 04/26] Android: Fix Saf Write test --- builds/android/app/src/main/java/org/easyrpg/player/Helper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java index 41acd3c00d..37bc969ee7 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java @@ -178,7 +178,7 @@ public static GameBrowserHelper.SafError testContentProvider(Context context, Ur return GameBrowserHelper.SafError.BAD_CONTENT_PROVIDER_READ; } - try (ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(testFile.getUri(), "r")) { + try (ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(testFile.getUri(), "w")) { } catch (IOException | IllegalArgumentException e) { return GameBrowserHelper.SafError.BAD_CONTENT_PROVIDER_WRITE; } From 59ac1ce90f665f6afadebef72a935a712d495162 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:51:43 +0200 Subject: [PATCH 05/26] Android: Refactor Player Filesystem API The JNIEnv-Pointer is now set from the outside. Makes it possible to call these functions without having SDL initialized. Remove DeleteLocalRef, as these references are garbage collected automatically when the function returns. --- CMakeLists.txt | 3 +- .../player/player/EasyRpgPlayerActivity.java | 6 +-- src/platform/android/android.cpp | 5 ++ src/platform/android/android.h | 6 ++- src/platform/android/filesystem_apk.cpp | 10 ++-- src/platform/android/filesystem_saf.cpp | 54 +++++-------------- src/platform/sdl/main.cpp | 5 ++ 7 files changed, 37 insertions(+), 52 deletions(-) create mode 100644 src/platform/android/android.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ac41b7dadb..69e1cdd57e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -573,7 +573,6 @@ if(${PLAYER_TARGET_PLATFORM} STREQUAL "SDL2") endif() if(ANDROID) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/builds/android/app/src/gamebrowser) set(PLAYER_BUILD_EXECUTABLE OFF) endif() @@ -1347,6 +1346,7 @@ else() # library endif() elseif(ANDROID AND ${PLAYER_TARGET_PLATFORM} STREQUAL "SDL2") add_library(easyrpg_android + src/platform/android/android.cpp src/platform/android/android.h src/platform/android/org_easyrpg_player_player_EasyRpgPlayerActivity.cpp src/platform/android/org_easyrpg_player_player_EasyRpgPlayerActivity.h @@ -1357,6 +1357,7 @@ else() # library src/platform/sdl/main.cpp) target_link_libraries(easyrpg_android ${PROJECT_NAME}) set_target_properties(easyrpg_android PROPERTIES DEBUG_POSTFIX "") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/builds/android/app/src/gamebrowser) set(PLAYER_TEST_LIBRARIES "easyrpg_android") else() message(FATAL_ERROR "Unsupported library target platform ${PLAYER_TARGET_PLATFORM}") diff --git a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java index 50a269321c..c50c4ab40c 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java @@ -364,7 +364,7 @@ public String getRtpPath() { return ""; } - public SafFile getHandleForPath(String path) { + public static SafFile getHandleForPath(String path) { return SafFile.fromPath(getContext(), path); } @@ -420,8 +420,8 @@ public void updateScreenPosition() { * * @return asset manager */ - public AssetManager getAssetManager() { - return getAssets(); + public static AssetManager getAssetManager() { + return getContext().getAssets(); } /** diff --git a/src/platform/android/android.cpp b/src/platform/android/android.cpp new file mode 100644 index 0000000000..27b32f4d30 --- /dev/null +++ b/src/platform/android/android.cpp @@ -0,0 +1,5 @@ +#include "android.h" + +JNIEnv* EpAndroid::env = nullptr; +std::function EpAndroid::android_fn; +std::mutex EpAndroid::android_mutex; diff --git a/src/platform/android/android.h b/src/platform/android/android.h index c20a0588ee..c133961772 100644 --- a/src/platform/android/android.h +++ b/src/platform/android/android.h @@ -20,10 +20,12 @@ #include #include +#include namespace EpAndroid { - inline std::function android_fn; - inline std::mutex android_mutex; + extern JNIEnv* env; + extern std::function android_fn; + extern std::mutex android_mutex; inline void invoke() { if (!android_fn) { diff --git a/src/platform/android/filesystem_apk.cpp b/src/platform/android/filesystem_apk.cpp index dd4df38f58..9f13dbb358 100644 --- a/src/platform/android/filesystem_apk.cpp +++ b/src/platform/android/filesystem_apk.cpp @@ -18,16 +18,16 @@ #include "filesystem_apk.h" #include "filefinder.h" #include "output.h" +#include "android.h" #include #include ApkFilesystem::ApkFilesystem() : Filesystem("", FilesystemView()) { - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); - jobject sdl_activity = (jobject)SDL_AndroidGetActivity(); - jclass cls = env->GetObjectClass(sdl_activity); - jmethodID jni_getAssetManager = env->GetMethodID(cls, "getAssetManager", "()Landroid/content/res/AssetManager;"); - jobject asset_manager = (jobject)env->CallObjectMethod(sdl_activity, jni_getAssetManager); + JNIEnv* env = EpAndroid::env; + jclass cls = env->FindClass("org/easyrpg/player/player/EasyRpgPlayerActivity"); + jmethodID jni_getAssetManager = env->GetStaticMethodID(cls, "getAssetManager", "()Landroid/content/res/AssetManager;"); + jobject asset_manager = (jobject)env->CallStaticObjectMethod(cls, jni_getAssetManager); mgr = AAssetManager_fromJava(env, asset_manager); } diff --git a/src/platform/android/filesystem_saf.cpp b/src/platform/android/filesystem_saf.cpp index 30febb5c2f..63403e459c 100644 --- a/src/platform/android/filesystem_saf.cpp +++ b/src/platform/android/filesystem_saf.cpp @@ -18,6 +18,7 @@ #include "filesystem_saf.h" #include "filefinder.h" #include "output.h" +#include "android.h" #include #include @@ -25,16 +26,11 @@ static jobject get_jni_handle(const SafFilesystem* fs, StringView path) { std::string combined_path = FileFinder::MakePath(fs->GetPath(), path); - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); - jobject sdl_activity = (jobject)SDL_AndroidGetActivity(); - jclass cls = env->GetObjectClass(sdl_activity); - jmethodID jni_getFilesystemForPath = env->GetMethodID(cls, "getHandleForPath", "(Ljava/lang/String;)Lorg/easyrpg/player/player/SafFile;"); + JNIEnv* env = EpAndroid::env; + jclass cls = env->FindClass("org/easyrpg/player/player/EasyRpgPlayerActivity"); + jmethodID jni_getFilesystemForPath = env->GetStaticMethodID(cls, "getHandleForPath", "(Ljava/lang/String;)Lorg/easyrpg/player/player/SafFile;"); jstring jpath = env->NewStringUTF(combined_path.c_str()); - jobject obj_res = env->CallObjectMethod(sdl_activity, jni_getFilesystemForPath, jpath); - - env->DeleteLocalRef(jpath); - env->DeleteLocalRef(cls); - env->DeleteLocalRef(sdl_activity); + jobject obj_res = env->CallStaticObjectMethod(cls, jni_getFilesystemForPath, jpath); return obj_res; } @@ -49,14 +45,11 @@ bool SafFilesystem::IsFile(StringView path) const { return false; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "isFile", "()Z"); jboolean res = env->CallBooleanMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - return res > 0; } @@ -66,14 +59,11 @@ bool SafFilesystem::IsDirectory(StringView dir, bool) const { return false; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "isDirectory", "()Z"); jboolean res = env->CallBooleanMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - return res > 0; } @@ -83,14 +73,11 @@ bool SafFilesystem::Exists(StringView filename) const { return false; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "exists", "()Z"); jboolean res = env->CallBooleanMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - return res > 0; } @@ -100,14 +87,11 @@ int64_t SafFilesystem::GetFilesize(StringView path) const { return -1; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "getFilesize", "()J"); jlong res = env->CallLongMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - return static_cast(res); } @@ -159,14 +143,11 @@ std::streambuf* SafFilesystem::CreateInputStreambuffer(StringView path, std::ios return nullptr; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "createInputFileDescriptor", "()I"); jint fd = env->CallIntMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - if (fd < 0) { return nullptr; } @@ -238,15 +219,12 @@ std::streambuf* SafFilesystem::CreateOutputStreambuffer(StringView path, std::io return nullptr; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = EpAndroid::env; jclass cls = env->GetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "createOutputFileDescriptor", "(Z)I"); jboolean append = static_cast(((mode & std::ios_base::app) == std::ios_base::app) ? 1u : 0u); jint fd = env->CallIntMethod(obj, jni_method, append); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - if (fd < 0) { return nullptr; } @@ -260,14 +238,11 @@ bool SafFilesystem::GetDirectoryContent(StringView path, std::vectorGetObjectClass(obj); jmethodID jni_method = env->GetMethodID(cls, "getDirectoryContent", "()Lorg/easyrpg/player/player/DirectoryTree;"); jobject directory_tree = env->CallObjectMethod(obj, jni_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(cls); - if (!directory_tree) { return false; } @@ -286,14 +261,11 @@ bool SafFilesystem::GetDirectoryContent(StringView path, std::vector(env->GetObjectArrayElement(names_arr, static_cast(i))); const char* str = env->GetStringUTFChars(elem, nullptr); entries.emplace_back(str, types[i] == 0 ? DirectoryTree::FileType::Regular : DirectoryTree::FileType::Directory); + // These are explicitly deleted, otherwise this garbabge collects after the loop env->ReleaseStringUTFChars(elem, str); env->DeleteLocalRef(elem); } - env->DeleteLocalRef(cls_directory_tree); - env->DeleteLocalRef(names_arr); - env->DeleteLocalRef(types_arr); - return true; } diff --git a/src/platform/sdl/main.cpp b/src/platform/sdl/main.cpp index 2183c87c9f..9924e4746c 100644 --- a/src/platform/sdl/main.cpp +++ b/src/platform/sdl/main.cpp @@ -30,6 +30,7 @@ # include #elif defined(__ANDROID__) # include +# include "platform/android/android.h" #elif defined(__WIIU__) # include #endif @@ -83,6 +84,10 @@ extern "C" int main(int argc, char* argv[]) { Output::SetLogCallback(LogCallback); #endif +#if defined(__ANDROID__) + EpAndroid::env = (JNIEnv*)SDL_AndroidGetJNIEnv(); +#endif + Player::Init(std::move(args)); Player::Run(); From 346506809a87e70075761ac22cde6417899e4709 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:56:52 +0200 Subject: [PATCH 06/26] Android: Use native FileFinder for the game detection of the GameBrowser. Allows to drop most of the Java scanning code and makes maintanance easier. --- .../app/src/gamebrowser/CMakeLists.txt | 5 +- ...asyrpg_player_game_browser_GameScanner.cpp | 119 +++--- ..._easyrpg_player_game_browser_GameScanner.h | 9 +- .../org/easyrpg/player/game_browser/Game.java | 102 +++-- .../game_browser/GameBrowserActivity.java | 4 +- .../game_browser/GameBrowserHelper.java | 95 ++--- .../player/game_browser/GameScanner.java | 356 +----------------- 7 files changed, 165 insertions(+), 525 deletions(-) diff --git a/builds/android/app/src/gamebrowser/CMakeLists.txt b/builds/android/app/src/gamebrowser/CMakeLists.txt index d578ec7b92..3782405e8b 100644 --- a/builds/android/app/src/gamebrowser/CMakeLists.txt +++ b/builds/android/app/src/gamebrowser/CMakeLists.txt @@ -7,8 +7,9 @@ add_library(gamebrowser org_easyrpg_player_game_browser_GameScanner.h ) -find_package(PNG REQUIRED) -target_link_libraries(gamebrowser PNG::PNG) +set_target_properties(gamebrowser PROPERTIES DEBUG_POSTFIX "") + +target_link_libraries(gamebrowser easyrpg_android) if(BUILD_SHARED_LIBS) set_property(TARGET gamebrowser PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp index 6a61c6e5ad..c1cc150ccc 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp @@ -31,41 +31,11 @@ #include #include -class FdStreamBufIn : public std::streambuf { -public: - FdStreamBufIn(int fd) : std::streambuf(), fd(fd) { - setg(buffer.data(), buffer.data() + buffer.size(), buffer.data() + buffer.size()); - } - - ~FdStreamBufIn() override { - close(fd); - } - - int underflow() override { - ssize_t res = read(fd, buffer.data(), buffer.size()); - if (res <= 0) { - return traits_type::eof(); - } - setg(buffer.data(), buffer.data(), buffer.data() + res); - return traits_type::to_int_type(*gptr()); - } - -private: - int fd = 0; - std::array buffer; -}; -class BufferStreamBufIn : public std::streambuf { -public: - BufferStreamBufIn(char* buffer, jsize size) : std::streambuf(), buffer(buffer), size(size) { - setg(buffer, buffer, buffer + size); - } - -private: - char* buffer; - jsize size; - jsize index = 0; -}; +#include "filefinder.h" +#include "utils.h" +#include "string_view.h" +#include "platform/android/android.h" // via https://stackoverflow.com/q/1821806 static void custom_png_write_func(png_structp png_ptr, png_bytep data, png_size_t length) { @@ -73,30 +43,6 @@ static void custom_png_write_func(png_structp png_ptr, png_bytep data, png_size p->insert(p->end(), data, data + length); } -jbyteArray readXyz(JNIEnv *env, std::istream& stream); - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_decodeXYZbuffer( - JNIEnv *env, jclass, jbyteArray buffer) { - jbyte* elements = env->GetByteArrayElements(buffer, nullptr); - jsize size = env->GetArrayLength(buffer); - - std::istream stream(new BufferStreamBufIn(reinterpret_cast(elements), size)); - jbyteArray array = readXyz(env, stream); - - env->ReleaseByteArrayElements(buffer, elements, 0); - - return array; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL Java_org_easyrpg_player_game_1browser_GameScanner_decodeXYZfd - (JNIEnv * env, jclass, jint fd) { - std::istream stream(new FdStreamBufIn(fd)); - return readXyz(env, stream); -} - jbyteArray readXyz(JNIEnv *env, std::istream& stream) { char header[4]; @@ -214,3 +160,60 @@ jbyteArray readXyz(JNIEnv *env, std::istream& stream) { return result; } + +extern "C" +JNIEXPORT jobject JNICALL +Java_org_easyrpg_player_game_1browser_GameScanner_findGame(JNIEnv *env, jclass clazz, + jstring path) { + EpAndroid::env = env; + + const char* cpath = env->GetStringUTFChars(path, nullptr); + std::string spath(cpath); + env->ReleaseStringUTFChars(path, cpath); + + auto fs = FileFinder::FindGameRecursive(FileFinder::Root().Create(spath)); + if (!fs) { + return nullptr; + } + + std::string save_path; + if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { + save_path = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); + + // compatibility with Java code + size_t ext = save_path.find('.'); + if (ext != std::string::npos) { + save_path = save_path.substr(0, ext); + } + } + + jbyteArray title_image = nullptr; + + auto title = fs.Subtree("Title"); + if (title) { + for (auto& [name, entry]: *title.ListDirectory()) { + if (entry.type == DirectoryTree::FileType::Regular) { + if (StringView(name).ends_with(".xyz")) { + auto is = title.OpenInputStream(entry.name); + title_image = readXyz(env, is); + } else if (StringView(name).ends_with(".png") || StringView(name).ends_with(".bmp")) { + auto is = title.OpenInputStream(entry.name); + auto vec = Utils::ReadStream(is); + + title_image = env->NewByteArray(vec.size()); + env->SetByteArrayRegion(title_image, 0, vec.size(), reinterpret_cast(vec.data())); + } + } + } + } + + jclass game_class = env->FindClass("org/easyrpg/player/game_browser/Game"); + + jmethodID constructor = env->GetMethodID(game_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); + + jstring game_path = env->NewStringUTF(("content://" + FileFinder::GetFullFilesystemPath(fs)).c_str()); + jstring save_pat = env->NewStringUTF(save_path.c_str()); + jobject game_object = env->NewObject(game_class, constructor, game_path, save_pat, title_image); + + return game_object; +} diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h index 893b672712..3743cc199f 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h @@ -8,12 +8,9 @@ extern "C" { #endif -JNIEXPORT jbyteArray JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_decodeXYZfd(JNIEnv *env, jclass clazz, jint fd); - -JNIEXPORT jbyteArray JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_decodeXYZbuffer(JNIEnv *env, jclass clazz, - jbyteArray buffer); +extern "C" +JNIEXPORT jobject JNICALL +Java_org_easyrpg_player_game_1browser_GameScanner_findGame(JNIEnv *env, jclass clazz, jstring path); #ifdef __cplusplus } 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 a408e495ab..f0ce844571 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 @@ -5,6 +5,7 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Base64; +import android.util.Log; import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; @@ -15,28 +16,43 @@ public class Game implements Comparable { final static char escapeCode = '\u0001'; - private final String title; + private String title; private final String gameFolderPath; private String savePath; private boolean isFavorite; - private final DocumentFile gameFolder; private Bitmap titleScreen; - /** Path to the game folder inside of the zip */ - private String zipInnerPath; - - private Game(DocumentFile gameFolder) { - this.gameFolder = gameFolder; - this.title = gameFolder.getName(); - Uri folderURI = gameFolder.getUri(); - this.gameFolderPath = folderURI.toString(); + private boolean standalone = false; + + private Game(String gameFolderPath) { + int encoded_slash_pos = gameFolderPath.lastIndexOf("%2F"); + if (encoded_slash_pos == -1) { + // Should not happen because the game is in a subdirectory + Log.e("EasyRPG", "Strange Uri " + gameFolderPath); + } + int slash_pos = gameFolderPath.indexOf("/", encoded_slash_pos); + + // A file is provided when a / is after the encoded / (%2F) + if (slash_pos > -1) { + // Extract the filename and properly encode it + this.title = gameFolderPath.substring(slash_pos + 1); + } else { + this.title = gameFolderPath.substring(encoded_slash_pos + 3); + } + + int dot_pos = this.title.indexOf("."); + if (dot_pos > -1) { + this.title = this.title.substring(0, dot_pos); + } + + this.gameFolderPath = gameFolderPath; this.savePath = gameFolderPath; this.isFavorite = isFavoriteFromSettings(); } - public Game(DocumentFile gameFolder, Bitmap titleScreen) { - this(gameFolder); - this.titleScreen = titleScreen; + public Game(String gameFolderPath, byte[] titleScreen) { + this(gameFolderPath); + this.titleScreen = BitmapFactory.decodeByteArray(titleScreen, 0, titleScreen.length);; } /** @@ -49,26 +65,22 @@ public Game(String gameFolder, String saveFolder) { this.title = "Standalone"; this.gameFolderPath = gameFolder; this.savePath = saveFolder; - this.gameFolder = null; this.isFavorite = false; + this.standalone = true; } - private Game(DocumentFile gameFolder, String pathInZip, Bitmap titleScreen) { - this(gameFolder, titleScreen); - zipInnerPath = pathInZip; - } - - public static Game fromZip(DocumentFile gameFolder, String pathInZip, String saveFolder, Bitmap titleScreen) { - Game game = new Game(gameFolder, pathInZip, titleScreen); + private Game(String gameFolderPath, String saveFolder, byte[] titleScreen) { + this(gameFolderPath, titleScreen); // is only relative here, launchGame will put this in the "saves" directory - game.setSavePath(saveFolder); - return game; + if (!saveFolder.isEmpty()) { + savePath = saveFolder; + } } public static Game fromCacheEntry(Context context, String cache) { String[] entries = cache.split(String.valueOf(escapeCode)); - if (entries.length != 5) { + if (entries.length != 3) { return null; } @@ -78,24 +90,12 @@ public static Game fromCacheEntry(Context context, String cache) { return null; } - boolean isZip = Boolean.parseBoolean(entries[2]); - String zipInnerPath = null; - - if (isZip) { - zipInnerPath = entries[3]; - } - - Bitmap titleScreen = null; - if (!entries[4].equals("null")) { - byte[] decodedByte = Base64.decode(entries[4], 0); - titleScreen = BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length); + byte[] titleScreen = null; + if (!entries[2].equals("null")) { + titleScreen = Base64.decode(entries[2], 0); } - if (isZip) { - return fromZip(gameFolder, zipInnerPath, savePath, titleScreen); - } else { - return new Game(gameFolder, titleScreen); - } + return new Game(entries[1], savePath, titleScreen); } public String getTitle() { @@ -162,13 +162,7 @@ public String toCacheEntry() { sb.append(savePath); sb.append(escapeCode); - sb.append(gameFolder.getUri()); - sb.append(escapeCode); - - sb.append(isZipArchive()); - sb.append(escapeCode); - - sb.append(zipInnerPath); + sb.append(gameFolderPath); sb.append(escapeCode); if (titleScreen != null) { @@ -183,23 +177,11 @@ public String toCacheEntry() { return sb.toString(); } - public DocumentFile getGameFolder() { - return gameFolder; - } - public Bitmap getTitleScreen() { return titleScreen; } public Boolean isStandalone() { - return gameFolder == null; - } - - public Boolean isZipArchive() { - return zipInnerPath != null; - } - - public String getZipInnerPath() { - return zipInnerPath; + return standalone; } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java index 7f9d2514ed..f623244169 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java @@ -57,10 +57,12 @@ protected void onCreate(Bundle savedInstanceState) { if (!libraryLoaded) { try { + System.loadLibrary("easyrpg_android"); System.loadLibrary("gamebrowser"); libraryLoaded = true; } catch (UnsatisfiedLinkError e) { - Log.e("EasyRPG Player", "Couldn't load libgamebrowser. XYZ parsing will be unavailable: " + e.getMessage()); + Log.e("EasyRPG Player", "Couldn't load libgamebrowser: " + e.getMessage()); + throw e; } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index 322e374518..47347ce528 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -50,74 +50,53 @@ public static void launchGame(Context context, Game game) { public static void launchGame(Context context, Game game, boolean debugMode) { String path = game.getGameFolderPath(); - // Test again in case somebody messed with the file system - boolean valid = game.isStandalone() || - (game.isZipArchive() && game.getGameFolder().canRead()) || - (game.getGameFolder().isDirectory() && game.getGameFolder().canRead()); - - if (valid) { - Intent intent = new Intent(context, EasyRpgPlayerActivity.class); - ArrayList args = new ArrayList<>(); - - // Command line passed via intent "command_line" - String savePath; - - if (game.isZipArchive()) { - // Create the redirected save folder - DocumentFile saveFolder = Helper.createFolderInSave(context, game.getSavePath()); - - args.add("--project-path"); - args.add(path + "/" + game.getZipInnerPath()); - - // In error case the native code will try to put a save folder next to the zip - if (saveFolder != null) { - savePath = saveFolder.getUri().toString(); - args.add("--save-path"); - args.add(savePath); - } else { - savePath = path; - } - } else { - args.add("--project-path"); - args.add(path); + Intent intent = new Intent(context, EasyRpgPlayerActivity.class); + ArrayList args = new ArrayList<>(); + + // Command line passed via intent "command_line" + args.add("--project-path"); + args.add(path); + + String savePath = path; + if (!game.getSavePath().isEmpty()) { + DocumentFile saveFolder = Helper.createFolderInSave(context, game.getSavePath()); - savePath = game.getSavePath(); + // In error case the native code will try to put a save folder next to the zip + if (saveFolder != null) { + savePath = saveFolder.getUri().toString(); args.add("--save-path"); args.add(savePath); } + } - Encoding enc = game.getEncoding(); - if (enc.getIndex() > 0) { - // 0 = Auto, in that case let the Player figure it out - args.add("--encoding"); - args.add(enc.getRegionCode()); - } + Encoding enc = game.getEncoding(); + if (enc.getIndex() > 0) { + // 0 = Auto, in that case let the Player figure it out + args.add("--encoding"); + args.add(enc.getRegionCode()); + } - args.add("--config-path"); - args.add(context.getExternalFilesDir(null).getAbsolutePath()); + args.add("--config-path"); + args.add(context.getExternalFilesDir(null).getAbsolutePath()); - // Soundfont - Uri soundfontUri = SettingsManager.getSoundFountFileURI(context); - if (soundfontUri != null) { - args.add("--soundfont"); - args.add(soundfontUri.toString()); - } + // Soundfont + Uri soundfontUri = SettingsManager.getSoundFountFileURI(context); + if (soundfontUri != null) { + args.add("--soundfont"); + args.add(soundfontUri.toString()); + } - if (debugMode) { - args.add("--test-play"); - } + if (debugMode) { + args.add("--test-play"); + } - intent.putExtra(EasyRpgPlayerActivity.TAG_SAVE_PATH, savePath); - intent.putExtra(EasyRpgPlayerActivity.TAG_COMMAND_LINE, args.toArray(new String[0])); - intent.putExtra(EasyRpgPlayerActivity.TAG_STANDALONE, game.isStandalone()); + intent.putExtra(EasyRpgPlayerActivity.TAG_SAVE_PATH, savePath); + intent.putExtra(EasyRpgPlayerActivity.TAG_COMMAND_LINE, args.toArray(new String[0])); + intent.putExtra(EasyRpgPlayerActivity.TAG_STANDALONE, game.isStandalone()); - Log.i("EasyRPG", "Start EasyRPG Player with following arguments : " + args); - Log.i("EasyRPG", "The RTP folder is : " + SettingsManager.getRTPFolderURI(context)); - context.startActivity(intent); - } else { - String msg = context.getString(R.string.not_valid_game).replace("$PATH", game.getTitle()); - Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); - } + Log.i("EasyRPG", "Start EasyRPG Player with following arguments : " + args); + Log.i("EasyRPG", "The RTP folder is : " + SettingsManager.getRTPFolderURI(context)); + context.startActivity(intent); } public static void openSettingsActivity(Context context) { 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 f10e6be13d..7242e21d78 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 @@ -16,6 +16,7 @@ import org.easyrpg.player.Helper; import org.easyrpg.player.R; import org.easyrpg.player.settings.SettingsManager; +import org.libsdl.app.SDL; import java.io.ByteArrayOutputStream; import java.io.File; @@ -51,7 +52,7 @@ private GameScanner() { this.errorList = new ArrayList<>(); } - public static GameScanner getInstance(Context context) { + public static GameScanner getInstance(Activity activity) { // Singleton pattern if (GameScanner.instance == null) { synchronized(GameScanner.class) { @@ -62,12 +63,14 @@ public static GameScanner getInstance(Context context) { } //Scan the folder - instance.scanGames(context); + instance.scanGames(activity); return instance; } - private void scanGames(Context context){ + private void scanGames(Activity activity){ + Context context = activity.getApplicationContext(); + gameList.clear(); errorList.clear(); @@ -118,7 +121,7 @@ private void scanGames(Context context){ } // Scan the games folder - scanFolderRecursive(context, gamesFolder.getUri(), GAME_SCANNING_DEPTH); + scanRootFolder(activity, gamesFolder.getUri()); // If the scan brings nothing in this folder : we notify the errorList if (gameList.size() <= 0) { @@ -148,289 +151,27 @@ private int scanFolderHash(Context context, Uri folderURI) { return sb.toString().hashCode(); } - private void scanFolderRecursive(Context context, Uri folderURI, int depth) { - if (depth > 0) { + private void scanRootFolder(Activity activity, Uri folderURI) { + Context context = activity.getApplicationContext(); + SDL.setContext(context); + for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { String fileDocumentID = array[0]; - String fileDocumentType = array[1]; String name = Helper.getFileNameFromDocumentID(fileDocumentID); - if (name.equals("")) { + if (name.isEmpty()) { continue; } + if (!name.startsWith(".")) { - boolean isDirectory = Helper.isDirectoryFromMimeType(fileDocumentType); - if (isDirectory) { - // Is the file/folder a RPG Maker game? Uri fileURI = Helper.getURIFromDocumentID(folderURI, fileDocumentID); - Game game = isAGame(context, fileURI); - if (game != null) { - gameList.add(game); - } - else if (depth > 1) { - // Not a RPG2k Game but a directory -> recurse - // (We don't check if it's a directory or if its readable because of slow - // Android SAF calls and scanFolder(...) already check that) - scanFolderRecursive(context, fileURI, depth - 1); - } - } else { - String nameLower = name.toLowerCase(Locale.ROOT); - if (nameLower.endsWith(".zip") || nameLower.endsWith(".easyrpg")) { - Uri fileURI = Helper.getURIFromDocumentID(folderURI, fileDocumentID); - Game game = isAGameZipped(context, fileURI, true); + Game game = findGame(fileURI.toString()); + if (game != null) { gameList.add(game); } } } - } - } - } - } - - /** Return a game if "folder" is a game folder, or return null. - * This method is designed to reduce the number of sys calls */ - public static Game isAGame(Context context, Uri uri) { - Game game = null; - Uri titleFolderURI = null; - - // Create a lookup by extension as we go, in case we are dealing with non-standard extensions. - int rpgRtCount = 0; - boolean databaseFound = false; - boolean treemapFound = false; - boolean isARpgGame = false; - - for (String filePath : Helper.listChildrenDocumentID(context, uri)) { - String fileName = Helper.getFileNameFromDocumentID(filePath); - String fileNameLower = fileName.toLowerCase(Locale.ROOT); - - if (!databaseFound && fileName.equals(DATABASE_NAME)) { - databaseFound = true; - } else if (!treemapFound && fileName.equals(TREEMAP_NAME)) { - treemapFound = true; - } - // Count non-standard files. - // NOTE: Do not put this in the 'else' statement, since only 1 extension may be non-standard and we want to count both. - // We might be dealing with a non-standard extension. - // Show it, and let the C++ code sort out which file is which. - if (fileNameLower.startsWith("rpg_rt.")) { - if (!(fileName.equals(INI_FILE) || fileName.equals(EXE_FILE))) { - rpgRtCount += 1; - } - } - - if ((databaseFound && treemapFound) || rpgRtCount == 2) { - isARpgGame = true; - } - - // If we encounter a Title folder, we keep it for the title screen - // We do that here in order to avoid syscalls - if (fileNameLower.equals("title")) { - titleFolderURI = DocumentsContract.buildDocumentUriUsingTree(uri, filePath); - } - } - - if (isARpgGame) { - Bitmap titleScreen = GameScanner.extractTitleScreenImage(context, titleFolderURI); - game = new Game(Helper.getFileFromURI(context, uri), titleScreen); - } - - return game; - } - - static class ZipFoundStats { - int rpgRtCount = 0; - boolean databaseFound = false; - boolean treemapFound = false; - boolean isARpgGame = false; - byte[] titleImage = null; - - ZipFoundStats() { - } - } - - /** Return a game if "folder" is a game folder, or return null. - * This method is designed to reduce the number of sys calls */ - public static Game isAGameZipped(Context context, Uri zipUri, boolean unicode) { - ContentResolver resolver = context.getContentResolver(); - - // Create a lookup by extension as we go, in case we are dealing with non-standard extensions. - Map games = new HashMap<>(); - - try (InputStream zipIStream = resolver.openInputStream(zipUri)) { - ZipInputStream zipStream; - if (unicode) { - zipStream = new ZipInputStream(zipIStream); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // arbitrary encoding that does not crash - zipStream = new ZipInputStream(zipIStream, StandardCharsets.ISO_8859_1); - } else { - return null; - } - - ZipEntry entry; - - while ((entry = zipStream.getNextEntry()) != null) { - if (entry.isDirectory()) { - continue; - } - - String fullPath = entry.getName(); - String fileName; - - if (fullPath.isEmpty()) { - continue; - } - - int slash = fullPath.lastIndexOf('/'); - if (slash == -1) { - slash = fullPath.lastIndexOf('\\'); - } - - if (slash == -1) { - fileName = fullPath; - } else if (slash == fullPath.length() - 1) { - continue; - } else { - fileName = fullPath.substring(slash + 1); - } - - String fileNameLower = fileName.toLowerCase(Locale.ROOT); - - String gameDirectory; - if (slash <= 0) { - gameDirectory = ""; - } else { - gameDirectory = fullPath.substring(0, slash); - } - - String gameDirectoryLower = gameDirectory.toLowerCase(Locale.ROOT); - if (gameDirectoryLower.endsWith("/title") || gameDirectoryLower.endsWith("\\title")) { - // Check for a title image - ZipFoundStats stats = games.get(gameDirectory.substring(0, gameDirectory.length() - "/title".length())); - - if (stats != null) { - if (fileNameLower.endsWith(".xyz") || fileNameLower.endsWith(".png") || fileNameLower.endsWith(".bmp")) { - int count; - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buffer = new byte[16 * 1024]; - while ((count = zipStream.read(buffer)) != -1) - out.write(buffer, 0, count); - stats.titleImage = out.toByteArray(); - if (stats.isARpgGame) { - break; - } - } - } - } - - continue; - } - - ZipFoundStats stats = games.get(gameDirectory); - - if (stats == null) { - stats = new ZipFoundStats(); - games.put(gameDirectory, stats); - } - - if (!stats.databaseFound && fileNameLower.equals(DATABASE_NAME)) { - stats.databaseFound = true; - } else if (!stats.treemapFound && fileNameLower.equalsIgnoreCase(TREEMAP_NAME)) { - stats.treemapFound = true; - } - - // Count non-standard files. - // NOTE: Do not put this in the 'else' statement, since only 1 extension may be non-standard and we want to count both. - // We might be dealing with a non-standard extension. - // Show it, and let the C++ code sort out which file is which. - if (fileNameLower.startsWith("rpg_rt.")) { - if (!(fileNameLower.equals(INI_FILE) || fileNameLower.equals(EXE_FILE))) { - stats.rpgRtCount += 1; - } - } - - if ((stats.databaseFound && stats.treemapFound) || stats.rpgRtCount == 2) { - stats.isARpgGame = true; - if (stats.titleImage != null) { - break; - } - } - } - } catch (IOException e) { - return null; - } catch (IllegalArgumentException e) { - if (unicode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return isAGameZipped(context, zipUri, false); - } - return null; - } - - for (Map.Entry entry : games.entrySet()) { - if (entry.getValue().isARpgGame) { - String name = new File(zipUri.getPath()).getName(); - String saveFolder = name.substring(0, name.lastIndexOf(".")); - Bitmap titleScreen = extractTitleScreenImage(context, entry.getValue().titleImage); - String key = entry.getKey(); - if (!unicode) { - // FIXME: This will launch the built-in Game Browser when the ZIP archive contains more than one folder in the root - // But the game can be at least launched this way. - key = ""; - } - return Game.fromZip(Helper.getFileFromURI(context, zipUri), key, saveFolder, titleScreen); - } - } - - return null; - } - - /** The VERY SLOW way of testing if a folder is a RPG2k Game. (contains DATABASE_NAME and TREEMAP_NAME) - * It shouldn't be use unless Google forces the use of DocumentFile over ContentResolver - * @param dir The directory to test - * @return true if RPG2k game - */ - @Deprecated - public static boolean isAGameSlowWay(DocumentFile dir) { - if (!dir.isDirectory() || !dir.canRead()) { - return false; - } - - boolean databaseFound = false; - boolean treemapFound = false; - - // Create a lookup by extension as we go, in case we are dealing with non-standard extensions. - int rpgRtCount = 0; - - for (DocumentFile entry : dir.listFiles()) { - if (entry.isFile() && entry.canRead()) { - String entryName = entry.getName(); - if (entryName == null) { - continue; - } - - if (!databaseFound && entryName.equalsIgnoreCase(DATABASE_NAME)) { - databaseFound = true; - } else if (!treemapFound && entryName.equalsIgnoreCase(TREEMAP_NAME)) { - treemapFound = true; - } - - // Count non-standard files. - // NOTE: Do not put this in the 'else' statement, since only 1 extension may be non-standard and we want to count both. - if (entryName.toLowerCase().startsWith("rpg_rt.")) { - if (!(entryName.equalsIgnoreCase(INI_FILE) || entryName.equalsIgnoreCase(EXE_FILE))) { - rpgRtCount += 1; - } - } - - if (databaseFound && treemapFound) { - return true; - } - } - } - - // We might be dealing with a non-standard extension. - // Show it, and let the C++ code sort out which file is which. - return rpgRtCount == 2; } public List getGameList() { @@ -445,70 +186,5 @@ public boolean hasError() { return !errorList.isEmpty(); } - /** Return the game title screen, in a dumb way following last Enterbrain conventions */ - public static Bitmap extractTitleScreenImage(Context context, byte[] titleScreenBuffer) { - if (titleScreenBuffer == null) { - return null; - } - - Bitmap bmp = BitmapFactory.decodeByteArray(titleScreenBuffer, 0, titleScreenBuffer.length); - - if (bmp == null) { - byte[] xyz = decodeXYZbuffer(titleScreenBuffer); - if (xyz == null) { - return null; - } - return BitmapFactory.decodeByteArray(xyz, 0, xyz.length); - } - - return bmp; - } - - /** Return the game title screen, in a dumb way following last Enterbrain conventions */ - public static Bitmap extractTitleScreenImage(Context context, Uri titleScreenFolderURI) { - try { - // Retrieve the Title folder, containing titles screens - DocumentFile titleFolder = Helper.getFileFromURI(context, titleScreenFolderURI); - - if (titleFolder != null && titleFolder.isDirectory()) { - - // Display the first image found in the Title folder - for (String fileID : Helper.listChildrenDocumentID(context, titleScreenFolderURI)) { - String fileName = Helper.getFileNameFromDocumentID(fileID).toLowerCase().trim(); - - if (!fileName.startsWith(".")) { - if (fileName.endsWith("png") || fileName.endsWith("bmp")) { - Uri imageUri = Helper.getURIFromDocumentID(titleScreenFolderURI, fileID); - return MediaStore.Images.Media.getBitmap(context.getContentResolver(), imageUri); - } - else if (fileName.endsWith("xyz")) { - Uri imageUri = Helper.getURIFromDocumentID(titleScreenFolderURI, fileID); - Bitmap b = MediaStore.Images.Media.getBitmap(context.getContentResolver(), imageUri); - if (b == null && GameBrowserActivity.libraryLoaded) { - // Check for XYZ - try (ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(imageUri, "r")) { - byte[] xyz = decodeXYZfd(fd.detachFd()); - if (xyz == null) { - return null; - } - return BitmapFactory.decodeByteArray(xyz, 0, xyz.length); - } catch (IOException e) { - return null; - } - } - return b; - } - } - } - } - } catch (Exception e) { - Log.e("EasyRPG", e.getMessage()); - } - - return null; - } - - private static native byte[] decodeXYZfd(int fd); - - private static native byte[] decodeXYZbuffer(byte[] buffer); + private static native Game findGame(String path); } From 9c99c324dc578fbd59553afd74061f99ca418234 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 12:57:27 +0200 Subject: [PATCH 07/26] Android: Add progress information to the scanning process --- .../player/game_browser/GameScanner.java | 43 +++++++++++++------ .../app/src/main/res/layout/loading_panel.xml | 23 ++++++++-- 2 files changed, 50 insertions(+), 16 deletions(-) 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 7242e21d78..3371e23979 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 @@ -1,5 +1,6 @@ package org.easyrpg.player.game_browser; +import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.graphics.Bitmap; @@ -10,6 +11,7 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.util.Log; +import android.widget.TextView; import androidx.documentfile.provider.DocumentFile; @@ -155,23 +157,38 @@ private void scanRootFolder(Activity activity, Uri folderURI) { Context context = activity.getApplicationContext(); SDL.setContext(context); - for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { - String fileDocumentID = array[0]; + final ArrayList names = new ArrayList<>(); + final ArrayList fileURIs = new ArrayList<>(); - String name = Helper.getFileNameFromDocumentID(fileDocumentID); + for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { + String fileDocumentID = array[0]; + + String name = Helper.getFileNameFromDocumentID(fileDocumentID); if (name.isEmpty()) { - continue; - } + continue; + } + + if (!name.startsWith(".")) { + Uri fileURI = Helper.getURIFromDocumentID(folderURI, fileDocumentID); + names.add(name); + fileURIs.add(fileURI); + } + } + + for (int i = 0; i < names.size(); ++i) { + final String name = names.get(i); + final int j = i; + activity.runOnUiThread(() -> { + TextView myTextView = activity.findViewById(R.id.progressText); + myTextView.setText(String.format("%s (%d/%d)", name, j + 1, names.size())); + }); - if (!name.startsWith(".")) { - Uri fileURI = Helper.getURIFromDocumentID(folderURI, fileDocumentID); - Game game = findGame(fileURI.toString()); + Game game = findGame(fileURIs.get(i).toString()); - if (game != null) { - gameList.add(game); - } - } - } + if (game != null) { + gameList.add(game); + } + } } public List getGameList() { diff --git a/builds/android/app/src/main/res/layout/loading_panel.xml b/builds/android/app/src/main/res/layout/loading_panel.xml index ccfd9190bd..370541bab5 100644 --- a/builds/android/app/src/main/res/layout/loading_panel.xml +++ b/builds/android/app/src/main/res/layout/loading_panel.xml @@ -1,8 +1,25 @@ - + + + From a2c0565995ad8945a7e127b4b3600302ff54677d Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 15:22:55 +0200 Subject: [PATCH 08/26] ZipFs: Reuse the initial stream (LzhFs already does this) --- src/filesystem_zip.cpp | 28 ++++++++++++++-------------- src/filesystem_zip.h | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/filesystem_zip.cpp b/src/filesystem_zip.cpp index b436a71e9a..68e5935100 100644 --- a/src/filesystem_zip.cpp +++ b/src/filesystem_zip.cpp @@ -54,8 +54,8 @@ static std::string normalize_path(StringView path) { ZipFilesystem::ZipFilesystem(std::string base_path, FilesystemView parent_fs, StringView enc) : Filesystem(base_path, parent_fs) { - auto zipfile = parent_fs.OpenInputStream(GetPath()); - if (!zipfile) { + zip_is = parent_fs.OpenInputStream(GetPath()); + if (!zip_is) { return; } @@ -70,18 +70,18 @@ ZipFilesystem::ZipFilesystem(std::string base_path, FilesystemView parent_fs, St bool is_utf8; encoding = ToString(enc); - if (!FindCentralDirectory(zipfile, central_directory_offset, central_directory_size, central_directory_entries)) { + if (!FindCentralDirectory(zip_is, central_directory_offset, central_directory_size, central_directory_entries)) { Output::Debug("ZipFS: {} is not a valid archive", GetPath()); return; } if (encoding.empty()) { - zipfile.seekg(central_directory_offset); + zip_is.seekg(central_directory_offset); std::stringstream filename_guess; // Guess the encoding first int items = 0; - while (ReadCentralDirectoryEntry(zipfile, filepath, entry, is_utf8)) { + while (ReadCentralDirectoryEntry(zip_is, filepath, entry, is_utf8)) { // Only consider Non-ASCII & Non-UTF8 for encoding detection // Skip directories, files already contain the paths if (is_utf8 || filepath.back() == '/' || Utils::StringIsAscii(filepath)) { @@ -124,13 +124,13 @@ ZipFilesystem::ZipFilesystem(std::string base_path, FilesystemView parent_fs, St } bool enc_is_utf8 = encoding == "UTF-8"; - zipfile.clear(); - zipfile.seekg(central_directory_offset); + zip_is.clear(); + zip_is.seekg(central_directory_offset); lcf::Encoder detected_encoder(encoding); lcf::Encoder cp437_encoder("437"); std::vector paths; - while (ReadCentralDirectoryEntry(zipfile, filepath, entry, is_utf8)) { + while (ReadCentralDirectoryEntry(zip_is, filepath, entry, is_utf8)) { if (is_utf8 || enc_is_utf8 || Utils::StringIsAscii(filepath)) { // No reencoding necessary filepath_cp437.clear(); @@ -358,11 +358,11 @@ std::streambuf* ZipFilesystem::CreateInputStreambuffer(StringView path, std::ios std::string path_normalized = normalize_path(path); auto central_entry = Find(path); if (central_entry && !central_entry->is_directory) { - auto zip_file = GetParent().OpenInputStream(GetPath()); - zip_file.seekg(central_entry->fileoffset); + zip_is.clear(); + zip_is.seekg(central_entry->fileoffset); StorageMethod method; ZipEntry local_entry = {}; - if (ReadLocalHeader(zip_file, method, local_entry)) { + if (ReadLocalHeader(zip_is, method, local_entry)) { if (central_entry->compressed_size != local_entry.compressed_size) { if (local_entry.compressed_size == 0) { local_entry.compressed_size = central_entry->compressed_size; @@ -386,15 +386,15 @@ std::streambuf* ZipFilesystem::CreateInputStreambuffer(StringView path, std::ios return nullptr; } - zip_file.seekg(central_entry->fileoffset + local_entry.fileoffset); + zip_is.seekg(central_entry->fileoffset + local_entry.fileoffset); if (method == StorageMethod::Plain) { auto data = std::vector(local_entry.uncompressed_size); - zip_file.read(reinterpret_cast(data.data()), data.size()); + zip_is.read(reinterpret_cast(data.data()), data.size()); return new Filesystem_Stream::InputMemoryStreamBuf(std::move(data)); } else if (method == StorageMethod::Deflate) { std::vector comp_buf; comp_buf.resize(local_entry.compressed_size); - zip_file.read(reinterpret_cast(comp_buf.data()), comp_buf.size()); + zip_is.read(reinterpret_cast(comp_buf.data()), comp_buf.size()); auto dec_buf = std::vector(local_entry.uncompressed_size); z_stream zlib_stream = {}; zlib_stream.next_in = reinterpret_cast(comp_buf.data()); diff --git a/src/filesystem_zip.h b/src/filesystem_zip.h index c5e5a2c71f..a613fc8afa 100644 --- a/src/filesystem_zip.h +++ b/src/filesystem_zip.h @@ -70,6 +70,7 @@ class ZipFilesystem : public Filesystem { std::vector> zip_entries; std::vector> zip_entries_cp437; std::string encoding; + mutable Filesystem_Stream::InputStream zip_is; mutable std::vector filename_buffer; }; From cc33823b0d6a7e9444aff0f8321b595f4145ec54 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 15:23:53 +0200 Subject: [PATCH 09/26] Android: Multiple games are now found in subdirectories This also allows stuff like multiple games in a single zip, zip inside zip etc. --- ...asyrpg_player_game_browser_GameScanner.cpp | 99 +++++++++++-------- ..._easyrpg_player_game_browser_GameScanner.h | 2 +- .../org/easyrpg/player/game_browser/Game.java | 23 +++-- .../player/game_browser/GameScanner.java | 17 ++-- src/filefinder.cpp | 41 ++++---- src/filefinder.h | 7 +- 6 files changed, 112 insertions(+), 77 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp index c1cc150ccc..88263c85b2 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp @@ -163,57 +163,78 @@ jbyteArray readXyz(JNIEnv *env, std::istream& stream) { extern "C" JNIEXPORT jobject JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_findGame(JNIEnv *env, jclass clazz, - jstring path) { +Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, + jstring jpath) { EpAndroid::env = env; - const char* cpath = env->GetStringUTFChars(path, nullptr); - std::string spath(cpath); - env->ReleaseStringUTFChars(path, cpath); + const char* path = env->GetStringUTFChars(jpath, nullptr); + std::string spath(path); + env->ReleaseStringUTFChars(jpath, path); - auto fs = FileFinder::FindGameRecursive(FileFinder::Root().Create(spath)); - if (!fs) { - return nullptr; + std::vector fs_list = FileFinder::FindGames(FileFinder::Root().Create(spath)); + + jclass jgame_class = env->FindClass("org/easyrpg/player/game_browser/Game"); + jobjectArray jgame_array = env->NewObjectArray(fs_list.size(), jgame_class, nullptr); + + if (fs_list.empty()) { + return jgame_array; } - std::string save_path; - if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { - save_path = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); + jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); + + for (size_t i = 0; i < fs_list.size(); ++i) { + auto& fs = fs_list[i]; + + std::string save_path; + if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { + // Is an archive and needs a redirected save path + save_path = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); - // compatibility with Java code - size_t ext = save_path.find('.'); - if (ext != std::string::npos) { - save_path = save_path.substr(0, ext); + // compatibility with original GameScanner Java code (everything after the extension dot is removed) + size_t ext = save_path.find('.'); + if (ext != std::string::npos) { + save_path = save_path.substr(0, ext); + } } - } - jbyteArray title_image = nullptr; - - auto title = fs.Subtree("Title"); - if (title) { - for (auto& [name, entry]: *title.ListDirectory()) { - if (entry.type == DirectoryTree::FileType::Regular) { - if (StringView(name).ends_with(".xyz")) { - auto is = title.OpenInputStream(entry.name); - title_image = readXyz(env, is); - } else if (StringView(name).ends_with(".png") || StringView(name).ends_with(".bmp")) { - auto is = title.OpenInputStream(entry.name); - auto vec = Utils::ReadStream(is); - - title_image = env->NewByteArray(vec.size()); - env->SetByteArrayRegion(title_image, 0, vec.size(), reinterpret_cast(vec.data())); + // Very simple title graphic search: The first image in "Title" is used + auto title = fs.Subtree("Title"); + jbyteArray title_image = nullptr; + if (title) { + for (auto &[name, entry]: *title.ListDirectory()) { + if (entry.type == DirectoryTree::FileType::Regular) { + if (StringView(name).ends_with(".xyz")) { + auto is = title.OpenInputStream(entry.name); + title_image = readXyz(env, is); + } else if (StringView(name).ends_with(".png") || + StringView(name).ends_with(".bmp")) { + auto is = title.OpenInputStream(entry.name); + if (!is) { + // When opening of the image fails it is an unsupported archive format + // Skip this game + continue; + } + + auto vec = Utils::ReadStream(is); + + title_image = env->NewByteArray(vec.size()); + env->SetByteArrayRegion(title_image, 0, vec.size(), + reinterpret_cast(vec.data())); + } } } } - } - jclass game_class = env->FindClass("org/easyrpg/player/game_browser/Game"); + // Create an instance of "Game" + jstring jgame_path = env->NewStringUTF( + ("content://" + FileFinder::GetFullFilesystemPath(fs)).c_str()); + jstring jsave_path = env->NewStringUTF(save_path.c_str()); + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor, jgame_path, jsave_path, title_image); - jmethodID constructor = env->GetMethodID(game_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); - - jstring game_path = env->NewStringUTF(("content://" + FileFinder::GetFullFilesystemPath(fs)).c_str()); - jstring save_pat = env->NewStringUTF(save_path.c_str()); - jobject game_object = env->NewObject(game_class, constructor, game_path, save_pat, title_image); + env->SetObjectArrayElement(jgame_array, i, jgame_object); + } - return game_object; + // Some fields of the Array can be NULL when a game was skipped due to an error + // This is sanitized on the Java site + return jgame_array; } diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h index 3743cc199f..861fe4dbf3 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h @@ -10,7 +10,7 @@ extern "C" { extern "C" JNIEXPORT jobject JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_findGame(JNIEnv *env, jclass clazz, jstring path); +Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring path); #ifdef __cplusplus } 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 f0ce844571..b165065359 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 @@ -16,23 +16,27 @@ public class Game implements Comparable { final static char escapeCode = '\u0001'; + /** The title shown in the Game Browser */ private String title; + /** Path to the game folder (forwarded via --project-path */ private final String gameFolderPath; + /** Relative path to the save directory, made absolute by launchGame (forwarded via --save-path) */ private String savePath; + /** Whether the game was tagged as a favourite */ private boolean isFavorite; - private Bitmap titleScreen; + /** Title image shown in the Game Browser */ + private Bitmap titleScreen = null; + /** Game is launched from the APK via standalone mode */ private boolean standalone = false; private Game(String gameFolderPath) { + // For simplicity the gameFolderPath is an URI that is parsed + // Similar to the SafFile code int encoded_slash_pos = gameFolderPath.lastIndexOf("%2F"); - if (encoded_slash_pos == -1) { - // Should not happen because the game is in a subdirectory - Log.e("EasyRPG", "Strange Uri " + gameFolderPath); - } - int slash_pos = gameFolderPath.indexOf("/", encoded_slash_pos); + int slash_pos = gameFolderPath.lastIndexOf("/"); // A file is provided when a / is after the encoded / (%2F) - if (slash_pos > -1) { + if (slash_pos > -1 && slash_pos > encoded_slash_pos) { // Extract the filename and properly encode it this.title = gameFolderPath.substring(slash_pos + 1); } else { @@ -41,6 +45,7 @@ private Game(String gameFolderPath) { int dot_pos = this.title.indexOf("."); if (dot_pos > -1) { + // Strip of the file extension this.title = this.title.substring(0, dot_pos); } @@ -52,7 +57,9 @@ private Game(String gameFolderPath) { public Game(String gameFolderPath, byte[] titleScreen) { this(gameFolderPath); - this.titleScreen = BitmapFactory.decodeByteArray(titleScreen, 0, titleScreen.length);; + if (titleScreen != null) { + this.titleScreen = BitmapFactory.decodeByteArray(titleScreen, 0, titleScreen.length); + }; } /** 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 3371e23979..8c24eee870 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 @@ -112,7 +112,7 @@ private void scanGames(Activity activity){ } } - if (gameList.size() > 0) { + if (!gameList.isEmpty()) { Log.i("EasyRPG", gameList.size() + " game(s) found in cache."); return; @@ -160,6 +160,7 @@ private void scanRootFolder(Activity activity, Uri folderURI) { final ArrayList names = new ArrayList<>(); final ArrayList fileURIs = new ArrayList<>(); + // Precalculate how many folders are to be scanned for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { String fileDocumentID = array[0]; @@ -175,18 +176,22 @@ private void scanRootFolder(Activity activity, Uri folderURI) { } } + // Scan all the folders and show the current scanning progress for (int i = 0; i < names.size(); ++i) { - final String name = names.get(i); + final String name = names.get(i); // only "final" variables can be passed to lambdas final int j = i; activity.runOnUiThread(() -> { + // Update Ui progress TextView myTextView = activity.findViewById(R.id.progressText); myTextView.setText(String.format("%s (%d/%d)", name, j + 1, names.size())); }); - Game game = findGame(fileURIs.get(i).toString()); + Game[] candidates = findGames(fileURIs.get(i).toString()); - if (game != null) { - gameList.add(game); + for (Game candidate: candidates) { + if (candidate != null) { + gameList.add(candidate); + } } } } @@ -203,5 +208,5 @@ public boolean hasError() { return !errorList.isEmpty(); } - private static native Game findGame(String path); + private static native Game[] findGames(String path); } diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 31fbad16db..9c893d6b26 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -533,30 +533,31 @@ void FileFinder::DumpFilesystem(FilesystemView fs) { } } -FilesystemView FileFinder::FindGameRecursive(FilesystemView fs, int recursion_limit) { - if (!fs || recursion_limit == 0) { - return {}; - } +std::vector FileFinder::FindGames(FilesystemView fs, int recursion_limit, int game_limit) { + std::vector games; - if (IsValidProject(fs)) { - return fs; - } + std::function find_recursive = [&](FilesystemView subfs, int rec_limit) -> void { + if (!subfs || rec_limit == 0 || games.size() >= game_limit) { + return; + } + + if (IsValidProject(subfs)) { + games.push_back(subfs); + return; + } - auto entries = fs.ListDirectory(); + auto entries = subfs.ListDirectory(); - for (auto& [name_lower, entry]: *entries) { - if (entry.type == DirectoryTree::FileType::Directory) { - auto fs_ret = FindGameRecursive(fs.Subtree(entry.name), recursion_limit - 1); - if (fs_ret) { - return fs_ret; - } - } else if (entry.type == DirectoryTree::FileType::Regular && IsSupportedArchiveExtension(entry.name)) { - auto fs_ret = FindGameRecursive(fs.Create(entry.name)); - if (fs_ret) { - return fs_ret; + for (auto& [name_lower, entry]: *entries) { + if (entry.type == DirectoryTree::FileType::Directory) { + find_recursive(subfs.Subtree(entry.name), rec_limit - 1); + } else if (entry.type == DirectoryTree::FileType::Regular && IsSupportedArchiveExtension(entry.name)) { + find_recursive(fs.Create(entry.name), rec_limit - 1); } } - } + }; + + find_recursive(fs, recursion_limit); - return {}; + return games; } diff --git a/src/filefinder.h b/src/filefinder.h index f747355eb3..0aa5eccdae 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -355,13 +355,14 @@ namespace FileFinder { void DumpFilesystem(FilesystemView fs); /** - * Searches recursively for a game directory. + * Searches recursively for game directories. * * @param fs Filesystem where the search starts * @param recursion_limit Recursion depth - * @return View of the game directory when found + * @param game_limit Abort the search when this amount of games was found. + * @return Vector of views to the found game directories */ - FilesystemView FindGameRecursive(FilesystemView fs, int recursion_limit = 3); + std::vector FindGames(FilesystemView fs, int recursion_limit = 3, int game_limit = 5); } // namespace FileFinder template From 440f32b25c24f330fbc2f67d682cbe8762cf40d1 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 15:35:20 +0200 Subject: [PATCH 10/26] Android: Sync the FF-Multiplier setting with the INI setting --- .../player/player/EasyRpgPlayerActivity.java | 3 --- .../org/easyrpg/player/settings/IniFile.java | 2 ++ .../easyrpg/player/settings/SettingsEnum.java | 4 ++-- .../settings/SettingsInputActivity.java | 6 ++--- .../player/settings/SettingsManager.java | 24 ++++++++++--------- .../res/layout/activity_settings_inputs.xml | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java index c50c4ab40c..be416e1521 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java @@ -153,9 +153,6 @@ public void onDrawerStateChanged(int newState) { mLayout.addView(surface); updateScreenPosition(); - // Set speed multiplier - setFastForwardMultiplier(SettingsManager.getFastForwardMultiplier()); - showInputLayout(); } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java index 9276bfde5b..35648fdcb3 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java @@ -10,6 +10,7 @@ public class IniFile { public SectionView video; public SectionView audio; + public SectionView input; public SectionView engine; public IniFile(File iniFile) { @@ -28,6 +29,7 @@ public IniFile(File iniFile) { video = new SectionView("Video"); audio = new SectionView("Audio"); + input = new SectionView("Input"); engine = new SectionView("Engine"); } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java index 5735bb6e47..d86951835d 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java @@ -17,13 +17,13 @@ enum SettingsEnum { CACHE_GAMES("PREF_CACHE_GAMES"), FORCED_LANDSCAPE("PREF_FORCED_LANDSCAPE"), FAST_FORWARD_MODE("FAST_FORWARD_MODE"), - FAST_FORWARD_MULTIPLIER("FAST_FORWARD_MULTIPLIER"), INPUT_LAYOUT_HORIZONTAL("INPUT_LAYOUT_HORIZONTAL"), INPUT_LAYOUT_VERTICAL("INPUT_LAYOUT_VERTICAL"), MUSIC_VOLUME("MusicVolume"), SOUND_VOLUME("SoundVolume"), STRETCH("Stretch"), - GAME_RESOLUTION("GameResolution") + GAME_RESOLUTION("GameResolution"), + SPEED_MODIFIER_A("SpeedModifierA") ; diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsInputActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsInputActivity.java index 006977f20a..924c3b4d62 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsInputActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsInputActivity.java @@ -76,13 +76,13 @@ public void onNothingSelected(AdapterView adapterView) { }); fastForwardMultiplierSeekBar = findViewById(R.id.settings_fast_forward_multiplier); - fastForwardMultiplierSeekBar.setProgress(SettingsManager.getFastForwardMultiplier() - 2); + fastForwardMultiplierSeekBar.setProgress(SettingsManager.getSpeedModifierA() - 2); fastForwardMultiplierSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { - // The seekbar has values 0-8, we want 2-10 - SettingsManager.setFastForwardMultiplier(seekBar.getProgress() + 2); + // The seekbar has values 0-98, we want 2-100 + SettingsManager.setSpeedModifierA(seekBar.getProgress() + 2); } @Override diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index fdcf398579..1e0f4d634e 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -37,6 +37,7 @@ public class SettingsManager { private static int imageSize, gameResolution; private static int layoutTransparency, layoutSize, fastForwardMode, fastForwardMultiplier; private static int musicVolume, soundVolume; + private static int speedModifierA; private static InputLayout inputLayoutHorizontal, inputLayoutVertical; // Note: don't store DocumentFile as they can be nullify with a change of context private static Uri easyRPGFolderURI, soundFountFileURI; @@ -76,11 +77,12 @@ private static void loadSettings(Context context) { forcedLandscape = sharedPref.getBoolean(FORCED_LANDSCAPE.toString(), false); stretch = configIni.video.getBoolean(STRETCH.toString(), false); fastForwardMode = sharedPref.getInt(FAST_FORWARD_MODE.toString(), FAST_FORWARD_MODE_TAP); - fastForwardMultiplier = sharedPref.getInt(FAST_FORWARD_MULTIPLIER.toString(), 3); musicVolume = configIni.audio.getInteger(MUSIC_VOLUME.toString(), 100); soundVolume = configIni.audio.getInteger(SOUND_VOLUME.toString(), 100); + speedModifierA = configIni.input.getInteger(SPEED_MODIFIER_A.toString(), 3); + favoriteGamesList = new HashSet<>(sharedPref.getStringSet(FAVORITE_GAMES.toString(), new HashSet<>())); gamesCacheHash = sharedPref.getInt(CACHE_GAMES_HASH.toString(), 0); @@ -197,16 +199,6 @@ public static void setFastForwardMode(int i) { editor.commit(); } - public static int getFastForwardMultiplier() { - return fastForwardMultiplier; - } - - public static void setFastForwardMultiplier(int i) { - fastForwardMultiplier = i; - editor.putInt(SettingsEnum.FAST_FORWARD_MULTIPLIER.toString(), i); - editor.commit(); - } - public static boolean isVibrateWhenSlidingDirectionEnabled() { return vibrateWhenSlidingDirectionEnabled; } @@ -398,4 +390,14 @@ public static void setGameEncoding(Game game, Encoding encoding) { editor.putString(game.getTitle() + "_Encoding", encoding.getRegionCode()); editor.commit(); } + + public static int getSpeedModifierA() { + return speedModifierA; + } + + public static void setSpeedModifierA(int speedModifierA) { + SettingsManager.speedModifierA = speedModifierA; + configIni.input.set(SPEED_MODIFIER_A.toString(), speedModifierA); + configIni.save(); + } } diff --git a/builds/android/app/src/main/res/layout/activity_settings_inputs.xml b/builds/android/app/src/main/res/layout/activity_settings_inputs.xml index 9035d58f88..29a6007cf4 100644 --- a/builds/android/app/src/main/res/layout/activity_settings_inputs.xml +++ b/builds/android/app/src/main/res/layout/activity_settings_inputs.xml @@ -63,7 +63,7 @@ android:id="@+id/settings_fast_forward_multiplier" android:layout_width="180dp" android:layout_height="wrap_content" - android:max="8" + android:max="98" android:progress="1" android:layout_alignParentEnd="true" android:layout_below="@id/settings_fast_forward" From 623cba0f368b524817ad2f631bb4548b8c4410f7 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 15:50:46 +0200 Subject: [PATCH 11/26] Android: Update translations to mention LZH support and lack of 7z-support --- builds/android/app/src/main/res/values-ca/strings.xml | 8 ++++---- builds/android/app/src/main/res/values-de/strings.xml | 6 +++--- builds/android/app/src/main/res/values-es/strings.xml | 8 ++++---- builds/android/app/src/main/res/values-fr/strings.xml | 8 ++++---- builds/android/app/src/main/res/values-in/strings.xml | 8 ++++---- builds/android/app/src/main/res/values-it/strings.xml | 5 +++-- builds/android/app/src/main/res/values-pl/strings.xml | 6 +++--- builds/android/app/src/main/res/values-zh/strings.xml | 8 ++++---- builds/android/app/src/main/res/values/strings.xml | 4 ++-- 9 files changed, 31 insertions(+), 30 deletions(-) diff --git a/builds/android/app/src/main/res/values-ca/strings.xml b/builds/android/app/src/main/res/values-ca/strings.xml index 95755889b7..3207614ba8 100644 --- a/builds/android/app/src/main/res/values-ca/strings.xml +++ b/builds/android/app/src/main/res/values-ca/strings.xml @@ -86,13 +86,13 @@ No s\'han trobat jocs de RPG Maker 2000/2003. \n \nLa carpeta de EasyRPG que has seleccionat conté una carpeta \"games\". Fes servir una aplicació de gestió de fitxers per posar els jocs en aquesta carpeta. -\nEls jocs es poden posar encarpetes o en arxius ZIP. +\nEls jocs es poden posar encarpetes o en arxius ZIP/LZH. \n \nLa carpeta EasyRPG es pot canviar a la configuració. Obre la carpeta de jocs (\"games\") Col·loca els jocs de RPG Maker 2000/2003 a subcarpetes de la carpeta de jocs (\"games\"). -\nEls jocs han de ser descomprimits o en un arxiu ZIP. -\nEls fitxers RAR, LZH i EXE NO estan suportats. +\nEls jocs han de ser descomprimits o en un arxiu ZIP/LZH. +\nEls fitxers RAR, 7z i EXE NO estan suportats. \n \nCaracterístiques opcionals (disponibles a la configuració): \n- Es pot afegir el RTP (arxius comuns usats per alguns jocs) @@ -148,4 +148,4 @@ \n mantenir tocar - \ No newline at end of file + diff --git a/builds/android/app/src/main/res/values-de/strings.xml b/builds/android/app/src/main/res/values-de/strings.xml index 506b6b803f..72b705100d 100644 --- a/builds/android/app/src/main/res/values-de/strings.xml +++ b/builds/android/app/src/main/res/values-de/strings.xml @@ -86,13 +86,13 @@ Es wurden keine RPG Maker 2000/2003-Spiele gefunden. \n \nDer von dir ausgewählte EasyRPG-Ordner enthält einen Spiele-Ordner (games). Verwende die Datei-App deines Systems um Spiele in diesem Ordner abzulegen. -\nSpiele können in Ordnern oder ZIP-Archiven sein. +\nSpiele können in Ordnern oder ZIP/LZH-Archiven sein. \n \nDu kannst den Pfad zum EasyRPG-Ordner in den Einstellungen ändern. Den Spiele-Ordner (games) öffnen RPG Maker 2000/2003-Spiele müssen im Unterverzeichnis \"games\" platziert werden. -\nDie Spiele müssen entpackt sein oder in einem ZIP-Archiv vorliegen. -\nRAR, LZH und EXE-Dateien werden NICHT unterstützt. +\nDie Spiele müssen entpackt sein oder in einem ZIP/LZH-Archiv vorliegen. +\nRAR-, 7z- und EXE-Dateien werden NICHT unterstützt. \n \nOptionale Funktionen (wirf einen Blick in die Einstellungen): \n- Das RTP (dies sind von einigen Spielen verwendete, gemeinsame Assets) kann bereitgestellt werden diff --git a/builds/android/app/src/main/res/values-es/strings.xml b/builds/android/app/src/main/res/values-es/strings.xml index f4da43cba8..4b30824bc6 100644 --- a/builds/android/app/src/main/res/values-es/strings.xml +++ b/builds/android/app/src/main/res/values-es/strings.xml @@ -86,13 +86,13 @@ No se han encontrado juegos de RPG Maker 2000/2003. \n \nLa carpeta de EasyRPG que has seleccionado contiene una carpeta \"games\". Utiliza una aplicación de gestión de ficheros para poner los juegos en esta carpeta. -\nLos juegos se pueden colocar en subcarpetas o en archivos ZIP. +\nLos juegos se pueden colocar en subcarpetas o en archivos ZIP/LZH. \n \nLa carpeta de EasyRPG se puede cambiar en la configuración. Abrir la carpeta de juegos (\"games\") Coloca los juegos de RPG Maker 2000/2003 en subcarpetas de la carpeta \"games\". -\nLos juegos tienen que estar descomprimidos o en un archivo ZIP. -\nLos archivos RAR, LZH y EXE NO están soportados. +\nLos juegos tienen que estar descomprimidos o en un archivo ZIP/LZH. +\nLos archivos RAR, 7z y EXE NO están soportados. \n \nCaracterísticas opcionales (disponibles en la configuración): \n- Se puede agregar el RTP (archivos comunes usados por algunos juegos) @@ -148,4 +148,4 @@ \n mantener tocar - \ No newline at end of file + diff --git a/builds/android/app/src/main/res/values-fr/strings.xml b/builds/android/app/src/main/res/values-fr/strings.xml index cbfda10e1f..cc1a5d09f6 100644 --- a/builds/android/app/src/main/res/values-fr/strings.xml +++ b/builds/android/app/src/main/res/values-fr/strings.xml @@ -82,13 +82,13 @@ Aucun jeu RPG Maker 2000/2003 n\'a été trouvé. \n \nLe dossier EasyRPG que vous avez sélectionné contient un dossier pour les jeux (games). Utiliser une application de gestionnaire de fichiers pour mettre vos jeux dans ce dossier. -\nLes jeux peuvent être placés dans des sous-répertoires ou dans des archives ZIP. +\nLes jeux peuvent être placés dans des sous-répertoires ou dans des archives ZIP/LZH. \n \nLe dossier d\'EasyRPG peut être changé dans les paramètres. Ouvrir le dossier « games » Mettez vos jeux RPG Maker 2000/2003 dans les sous-répertoires du dossier « games ». -\nLes jeux doivent être décompressés ou dans une archive ZIP. -\nLes fichiers RAR, LZH et EXE ne sont PAS supportés. +\nLes jeux doivent être décompressés ou dans une archive ZIP/LZH. +\nLes fichiers RAR, 7z et EXE ne sont PAS supportés. \n \nFonctionnalités optionnelles (Veuillez vous référer aux paramètres) : \n- Le RTP (ressources partagées utilisées par certains jeux) peut être fourni @@ -144,4 +144,4 @@ \n maintenir appuyer - \ No newline at end of file + diff --git a/builds/android/app/src/main/res/values-in/strings.xml b/builds/android/app/src/main/res/values-in/strings.xml index aecbb9d296..82b74d4a55 100644 --- a/builds/android/app/src/main/res/values-in/strings.xml +++ b/builds/android/app/src/main/res/values-in/strings.xml @@ -85,12 +85,12 @@ Buka menu Android Game RPG Maker 2000/2003 tidak ditemukan. \nFolder EasyRPG yang Anda pilih berisi folder \"games\". Gunakan aplikasi file manager untuk meletakkan game mu di folder ini. -\nGame dapat dimasukkan ke dalam subfolder atau di arsip ZIP. +\nGame dapat dimasukkan ke dalam subfolder atau di arsip ZIP/LZH. \nFolder EasyRPG bisa diubah di pengaturan. Buka folder \"games\" Letakkan permainan RPG Maker 2000/2003 di subdirektori folder \"games\". -\nPermainan harus sudah tidak terkompres. -\nfile ZIP, RAR, LZH dan EXE TIDAK didukung. +\nPermainan harus sudah tidak terkompres atau dalam arsip ZIP/LZH. +\nfile RAR, 7z dan EXE TIDAK didukung. \n \nFitur opsional (silahkan lihat pengaturan): \n- RTP (aset bersama digunakan oleh beberapa permainan) yang disediakan @@ -146,4 +146,4 @@ \n tahan tekan - \ No newline at end of file + diff --git a/builds/android/app/src/main/res/values-it/strings.xml b/builds/android/app/src/main/res/values-it/strings.xml index e695594b70..695d96be09 100644 --- a/builds/android/app/src/main/res/values-it/strings.xml +++ b/builds/android/app/src/main/res/values-it/strings.xml @@ -84,10 +84,11 @@ Chiudi menù di navigazione Apri Menù Android Non sono stati trovati giochi creati con RPG Maker 2000/2003. Mettili nella sottosezione della cartella giochi. -\nI giochi posso essere in cartelle o in archivi ZIP. La cartella di EasyRPG può essere modificata direttamente dalle Impostazioni. +\nI giochi posso essere in cartelle o in archivi ZIP/LZH. La cartella di EasyRPG può essere modificata direttamente dalle Impostazioni. Apri la cartella dei giochi Inserisci i giochi creati con RPG Maker 2000/2003 nella sottosezione della cartella dei giochi. -\nI giochi possono essere in cartelle o in formato ZIP. +\nI giochi possono essere in cartelle o in un archivio ZIP/LZH. +\nI file RAR, 7z ed EXE NON sono supportati \n \nFunzioni opzionali (Attivabili dalle Impostazioni): \n- Il RPT (assets usati da altri giochi) possono essere utilizzati diff --git a/builds/android/app/src/main/res/values-pl/strings.xml b/builds/android/app/src/main/res/values-pl/strings.xml index 929e38176a..50c399f211 100644 --- a/builds/android/app/src/main/res/values-pl/strings.xml +++ b/builds/android/app/src/main/res/values-pl/strings.xml @@ -84,11 +84,11 @@ Zamknij menu nawigacji Otwórz menu Androida Nie znaleziono gier RPG Maker 2000/2003. Umieść je w podkatalogach folderu gier. -\nGry mogą być w osobnych katalogach lub w archiwach ZIP. Folder EasyRPG można zmienić w ustawieniach. +\nGry mogą być w osobnych katalogach lub w archiwach ZIP/LZH. Folder EasyRPG można zmienić w ustawieniach. Otwórz folder gier Umieść swoje gry RPG Maker 2000/2003 w podkatalogach folderu gier. -\nGry muszą być nieskompresowane bądź spakowane w archiwum ZIP. -\nPliki RAR, LZH i EXE NIE są obsługiwane. +\nGry muszą być nieskompresowane bądź spakowane w archiwum ZIP/LZH. +\nPliki RAR, 7t i EXE NIE są obsługiwane. \n \nFunkcje opcjonalne (zapoznaj się z ustawieniami): \n- RTP (współdzielone zasoby używane przez niektóre gry) mogą być dostarczone diff --git a/builds/android/app/src/main/res/values-zh/strings.xml b/builds/android/app/src/main/res/values-zh/strings.xml index b0125bb81f..a1f6915953 100644 --- a/builds/android/app/src/main/res/values-zh/strings.xml +++ b/builds/android/app/src/main/res/values-zh/strings.xml @@ -109,12 +109,12 @@ 找不到 RPG Maker 2000/2003 游戏。 \n \n您选择的 EasyRPG 文件夹包含一个 \"games\" 文件夹。使用文件管理器应用程序将您的游戏放在此文件夹中。 -\n游戏可以放在子文件夹或 ZIP 档案中。 +\n游戏可以放在子文件夹或 ZIP/LZH 档案中。 \n \nEasyRPG 文件夹可以在设置中更改。 将您的 RPG Maker 2000/200 3游戏放在“游戏”文件夹的子目录中。 -\n游戏必须解压缩或保存在 ZIP 档案中。 -\n不支持 RAR、LZH 和 EXE 文件。 +\n游戏必须解压缩或保存在 ZIP/LZH 档案中。 +\n不支持 RAR、7z 和 EXE 文件。 \n \n可选功能(请参阅设置): \n-RTP(一些游戏所使用的共享素材) @@ -144,4 +144,4 @@ \n 长按 点击 - \ No newline at end of file + diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index fc7ba497ed..6d92b8777c 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ No Creating $PATH directory failed $PATH not readable - No RPG Maker 2000/2003 games found.\n\nThe EasyRPG folder you selected contains a \"games\" folder. Use a file manager app to put your games in this folder.\nGames can be put in subfolders or in ZIP archives.\n\nThe EasyRPG folder can be changed in the settings. + No RPG Maker 2000/2003 games found.\n\nThe EasyRPG folder you selected contains a \"games\" folder. Use a file manager app to put your games in this folder.\nGames can be put in subfolders or in ZIP/LZH archives.\n\nThe EasyRPG folder can be changed in the settings. Open the \"games\" folder No external storage (e.g. SD card) found $PATH is not a valid game @@ -37,7 +37,7 @@ Refresh Change the default button mapping How to use EasyRPG Player - Put your RPG Maker 2000/2003 games in subdirectories of the \"games\" folder.\nGames have to be uncompressed or in a ZIP archive.\nRAR, LZH and EXE files are NOT supported.\n\nOptional features (Please refer to the settings):\n- The RTP (shared assets used by some games) can be provided\n- A custom soundfont to alter how MIDI music sounds can be provided + Put your RPG Maker 2000/2003 games in subdirectories of the \"games\" folder.\nGames have to be uncompressed or in a ZIP/LZH archive.\nRAR, 7z and EXE files are NOT supported.\n\nOptional features (Please refer to the settings):\n- The RTP (shared assets used by some games) can be provided\n- A custom soundfont to alter how MIDI music sounds can be provided Visit our website Getting started and adding games (Video Guide) The \"games\" folder does not exist or is not a folder. Please create it manually or select a different location for the EasyRPG folder. The EasyRPG folder can be changed in the settings. From e229a20eafbda50875101352d6cadeb666915e06 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 20:32:49 +0200 Subject: [PATCH 12/26] Android: Pass in the title through JNI, cleanup --- ...asyrpg_player_game_browser_GameScanner.cpp | 46 +++-- ..._easyrpg_player_game_browser_GameScanner.h | 2 +- .../main/java/org/easyrpg/player/Helper.java | 28 ++- .../java/org/easyrpg/player/InitActivity.java | 3 +- .../org/easyrpg/player/game_browser/Game.java | 160 +++++++----------- .../game_browser/GameBrowserActivity.java | 2 +- .../game_browser/GameBrowserHelper.java | 3 +- .../player/game_browser/GameScanner.java | 28 +-- .../settings/SettingsAudioActivity.java | 6 +- .../player/settings/SettingsManager.java | 2 +- 10 files changed, 122 insertions(+), 158 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp index 88263c85b2..1cf15c8f6b 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp @@ -163,15 +163,15 @@ jbyteArray readXyz(JNIEnv *env, std::istream& stream) { extern "C" JNIEXPORT jobject JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, - jstring jpath) { +Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring jpath, jstring jmain_dir_name) { EpAndroid::env = env; const char* path = env->GetStringUTFChars(jpath, nullptr); std::string spath(path); env->ReleaseStringUTFChars(jpath, path); - std::vector fs_list = FileFinder::FindGames(FileFinder::Root().Create(spath)); + auto root = FileFinder::Root().Create(spath); + std::vector fs_list = FileFinder::FindGames(root); jclass jgame_class = env->FindClass("org/easyrpg/player/game_browser/Game"); jobjectArray jgame_array = env->NewObjectArray(fs_list.size(), jgame_class, nullptr); @@ -180,15 +180,34 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass return jgame_array; } - jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); + jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V"); + + bool game_in_main_dir = false; + if (fs_list.size() == 1) { + if (FileFinder::GetFullFilesystemPath(root) == FileFinder::GetFullFilesystemPath(fs_list[0])) { + game_in_main_dir = true; + } + } for (size_t i = 0; i < fs_list.size(); ++i) { auto& fs = fs_list[i]; + std::string full_path = FileFinder::GetFullFilesystemPath(fs); + std::string title; + if (game_in_main_dir) { + // The main dir is URI encoded, the human readable name is in jmain_dir_name + const char* main_dir_name = env->GetStringUTFChars(jmain_dir_name, nullptr); + title = main_dir_name; + env->ReleaseStringUTFChars(jmain_dir_name, main_dir_name); + } else { + // In all other cases the folder name is "clean" and can be used + title = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); + } + std::string save_path; if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { // Is an archive and needs a redirected save path - save_path = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); + save_path = title; // compatibility with original GameScanner Java code (everything after the extension dot is removed) size_t ext = save_path.find('.'); @@ -198,17 +217,17 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } // Very simple title graphic search: The first image in "Title" is used - auto title = fs.Subtree("Title"); + auto title_fs = fs.Subtree("Title"); jbyteArray title_image = nullptr; - if (title) { - for (auto &[name, entry]: *title.ListDirectory()) { + if (title_fs) { + for (auto &[name, entry]: *title_fs.ListDirectory()) { if (entry.type == DirectoryTree::FileType::Regular) { if (StringView(name).ends_with(".xyz")) { - auto is = title.OpenInputStream(entry.name); + auto is = title_fs.OpenInputStream(entry.name); title_image = readXyz(env, is); } else if (StringView(name).ends_with(".png") || StringView(name).ends_with(".bmp")) { - auto is = title.OpenInputStream(entry.name); + auto is = title_fs.OpenInputStream(entry.name); if (!is) { // When opening of the image fails it is an unsupported archive format // Skip this game @@ -216,7 +235,6 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } auto vec = Utils::ReadStream(is); - title_image = env->NewByteArray(vec.size()); env->SetByteArrayRegion(title_image, 0, vec.size(), reinterpret_cast(vec.data())); @@ -226,10 +244,10 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } // Create an instance of "Game" - jstring jgame_path = env->NewStringUTF( - ("content://" + FileFinder::GetFullFilesystemPath(fs)).c_str()); + jstring jgame_path = env->NewStringUTF(("content://" + full_path).c_str()); jstring jsave_path = env->NewStringUTF(save_path.c_str()); - jobject jgame_object = env->NewObject(jgame_class, jgame_constructor, jgame_path, jsave_path, title_image); + jstring jtitle = env->NewStringUTF(title.c_str()); + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor, jgame_path, jsave_path, jtitle, title_image); env->SetObjectArrayElement(jgame_array, i, jgame_object); } diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h index 861fe4dbf3..95e56ade0a 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h @@ -10,7 +10,7 @@ extern "C" { extern "C" JNIEXPORT jobject JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring path); +Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring path, jstring jmain_dir_name); #ifdef __cplusplus } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java index 37bc969ee7..7a49df8e9d 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java @@ -29,7 +29,6 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; -import java.util.Set; public class Helper { /** @@ -215,17 +214,21 @@ public static List listChildrenDocumentID(Context context, Uri folderUri return filesList; } - /** List files (with DOCUMENT_ID and MIME_TYPE) in the folder pointed by "folderURI" */ - public static List listChildrenDocumentIDAndType(Context context, Uri folderUri){ + /** + * List files in the folder pointed by "folderURI" + * @return Array of Document ID, mimeType, display name (filename) + */ + public static List listChildrenDocuments(Context context, Uri folderUri){ final ContentResolver resolver = context.getContentResolver(); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, DocumentsContract.getDocumentId(folderUri)); List filesList = new ArrayList<>(); try { - Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null); + Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null); while (c.moveToNext()) { String documentID = c.getString(0); String mimeType = c.getString(1); - filesList.add(new String[] {documentID, mimeType}); + String fileName = c.getString(2); + filesList.add(new String[] {documentID, mimeType, fileName}); } c.close(); } catch (Exception e) { @@ -238,10 +241,10 @@ public static Uri findFileUri(Context context, Uri folderUri, String fileNameToF final ContentResolver resolver = context.getContentResolver(); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, DocumentsContract.getDocumentId(folderUri)); try { - Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null); while (c.moveToNext()) { String documentID = c.getString(0); - String fileName = getFileNameFromDocumentID(documentID); + String fileName = c.getString(1); if (fileName.equals(fileNameToFind)) { Uri uri = DocumentsContract.buildDocumentUriUsingTree(folderUri, documentID); c.close(); @@ -261,10 +264,10 @@ public static List findFileUriWithRegex(Context context, Uri folderUri, Str final ContentResolver resolver = context.getContentResolver(); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, DocumentsContract.getDocumentId(folderUri)); try { - Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null); while (c.moveToNext()) { String documentID = c.getString(0); - String fileName = getFileNameFromDocumentID(documentID); + String fileName = c.getString(1); if (fileName.matches(regex)) { Uri uri = DocumentsContract.buildDocumentUriUsingTree(folderUri, documentID); uriList.add(uri); @@ -282,13 +285,6 @@ public static DocumentFile findFile(Context context, Uri folderUri, String fileN return getFileFromURI(context, uri); } - public static String getFileNameFromDocumentID(String documentID) { - if (documentID != null) { - return documentID.substring(documentID.lastIndexOf('/') + 1); - } - return ""; - } - public static DocumentFile getFileFromURI (Context context, Uri fileURI) { try { return DocumentFile.fromTreeUri(context, fileURI); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java index 2f49666810..9a8d95202e 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java @@ -125,7 +125,8 @@ private void startGameStandalone() { String saveDir = getExternalFilesDir(null).getAbsolutePath() + "/Save"; new File(saveDir).mkdirs(); - Game project = new Game(gameDir, saveDir); + Game project = new Game(gameDir, saveDir, "", null); + project.setStandalone(true); GameBrowserHelper.launchGame(this, project); finish(); } 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 b165065359..ff836836e4 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 @@ -17,77 +17,39 @@ public class Game implements Comparable { final static char escapeCode = '\u0001'; /** The title shown in the Game Browser */ - private String title; + private String title; /** Path to the game folder (forwarded via --project-path */ private final String gameFolderPath; /** Relative path to the save directory, made absolute by launchGame (forwarded via --save-path) */ - private String savePath; + private String savePath = ""; /** Whether the game was tagged as a favourite */ - private boolean isFavorite; + private boolean isFavorite; /** Title image shown in the Game Browser */ private Bitmap titleScreen = null; /** Game is launched from the APK via standalone mode */ private boolean standalone = false; - private Game(String gameFolderPath) { - // For simplicity the gameFolderPath is an URI that is parsed - // Similar to the SafFile code - int encoded_slash_pos = gameFolderPath.lastIndexOf("%2F"); - int slash_pos = gameFolderPath.lastIndexOf("/"); - - // A file is provided when a / is after the encoded / (%2F) - if (slash_pos > -1 && slash_pos > encoded_slash_pos) { - // Extract the filename and properly encode it - this.title = gameFolderPath.substring(slash_pos + 1); - } else { - this.title = gameFolderPath.substring(encoded_slash_pos + 3); - } + public Game(String gameFolderPath, String saveFolder, String title, byte[] titleScreen) { + this.gameFolderPath = gameFolderPath; - int dot_pos = this.title.indexOf("."); - if (dot_pos > -1) { - // Strip of the file extension - this.title = this.title.substring(0, dot_pos); + // is only relative here, launchGame will put this in the "saves" directory + if (!saveFolder.isEmpty()) { + savePath = saveFolder; } - this.gameFolderPath = gameFolderPath; - this.savePath = gameFolderPath; + this.title = title; - this.isFavorite = isFavoriteFromSettings(); - } - - public Game(String gameFolderPath, byte[] titleScreen) { - this(gameFolderPath); if (titleScreen != null) { this.titleScreen = BitmapFactory.decodeByteArray(titleScreen, 0, titleScreen.length); }; - } - /** - * Constructor for standalone mode - * - * @param gameFolder - * @param saveFolder - */ - public Game(String gameFolder, String saveFolder) { - this.title = "Standalone"; - this.gameFolderPath = gameFolder; - this.savePath = saveFolder; - this.isFavorite = false; - this.standalone = true; - } - - private Game(String gameFolderPath, String saveFolder, byte[] titleScreen) { - this(gameFolderPath, titleScreen); - // is only relative here, launchGame will put this in the "saves" directory - if (!saveFolder.isEmpty()) { - savePath = saveFolder; - } + this.isFavorite = isFavoriteFromSettings(); } public static Game fromCacheEntry(Context context, String cache) { String[] entries = cache.split(String.valueOf(escapeCode)); - if (entries.length != 3) { + if (entries.length != 4) { return null; } @@ -97,65 +59,67 @@ public static Game fromCacheEntry(Context context, String cache) { return null; } + String title = entries[2]; + byte[] titleScreen = null; - if (!entries[2].equals("null")) { - titleScreen = Base64.decode(entries[2], 0); + if (!entries[3].equals("null")) { + titleScreen = Base64.decode(entries[3], 0); } - return new Game(entries[1], savePath, titleScreen); + return new Game(entries[1], savePath, title, titleScreen); } - public String getTitle() { - return title; - } + public String getTitle() { + return title; + } - public String getGameFolderPath() { - return gameFolderPath; - } + public String getGameFolderPath() { + return gameFolderPath; + } - public String getSavePath() { - return savePath; - } + public String getSavePath() { + return savePath; + } public void setSavePath(String path) { savePath = path; } - public boolean isFavorite() { - return isFavorite; - } - - public void setFavorite(boolean isFavorite) { - this.isFavorite = isFavorite; - if(isFavorite){ - SettingsManager.addFavoriteGame(this); - } else { - SettingsManager.removeAFavoriteGame(this); - } - } - - private boolean isFavoriteFromSettings() { - return SettingsManager.getFavoriteGamesList().contains(this.title); - } - - @Override - public int compareTo(Game game) { - if (this.isFavorite() && !game.isFavorite()) { - return -1; - } - if (!this.isFavorite() && game.isFavorite()) { - return 1; - } - return this.title.compareTo(game.title); - } - - public Encoding getEncoding() { + public boolean isFavorite() { + return isFavorite; + } + + public void setFavorite(boolean isFavorite) { + this.isFavorite = isFavorite; + if(isFavorite){ + SettingsManager.addFavoriteGame(this); + } else { + SettingsManager.removeAFavoriteGame(this); + } + } + + private boolean isFavoriteFromSettings() { + return SettingsManager.getFavoriteGamesList().contains(this.gameFolderPath); + } + + @Override + public int compareTo(Game game) { + if (this.isFavorite() && !game.isFavorite()) { + return -1; + } + if (!this.isFavorite() && game.isFavorite()) { + return 1; + } + return this.title.compareTo(game.title); + } + + public Encoding getEncoding() { return SettingsManager.getGameEncoding(this); - } + } - public void setEncoding(Encoding encoding) { + public void setEncoding(Encoding encoding) { SettingsManager.setGameEncoding(this, encoding); - } + } @NonNull @Override @@ -166,11 +130,13 @@ public String toString() { public String toCacheEntry() { StringBuilder sb = new StringBuilder(); + // Cache structure: savePath | gameFolderPath | title | titleScreen sb.append(savePath); sb.append(escapeCode); - sb.append(gameFolderPath); sb.append(escapeCode); + sb.append(title); + sb.append(escapeCode); if (titleScreen != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -188,7 +154,11 @@ public Bitmap getTitleScreen() { return titleScreen; } - public Boolean isStandalone() { + public boolean isStandalone() { return standalone; } + + public void setStandalone(boolean standalone) { + this.standalone = standalone; + } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java index f623244169..5e55c47521 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java @@ -62,7 +62,7 @@ protected void onCreate(Bundle savedInstanceState) { libraryLoaded = true; } catch (UnsatisfiedLinkError e) { Log.e("EasyRPG Player", "Couldn't load libgamebrowser: " + e.getMessage()); - throw e; + throw e; } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index 47347ce528..e8f20f56ca 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -8,7 +8,6 @@ import android.net.Uri; import android.preference.PreferenceManager; import android.util.Log; -import android.widget.Toast; import androidx.documentfile.provider.DocumentFile; @@ -178,7 +177,7 @@ public static SafError dealAfterFolderSelected(Activity activity, int requestCod return SafError.BAD_CONTENT_PROVIDER_BASE_FOLDER_NOT_FOUND; } - List items = Helper.listChildrenDocumentIDAndType(activity, folder.getUri()); + List items = Helper.listChildrenDocuments(activity, folder.getUri()); int item_count = 0; for (String[] item: items) { if (item[0] == null || Helper.isDirectoryFromMimeType(item[1]) || item[0].endsWith(".nomedia")) { 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 8c24eee870..628089d841 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 @@ -1,15 +1,8 @@ package org.easyrpg.player.game_browser; import android.app.Activity; -import android.content.ContentResolver; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; -import android.provider.MediaStore; import android.util.Log; import android.widget.TextView; @@ -20,21 +13,10 @@ import org.easyrpg.player.settings.SettingsManager; import org.libsdl.app.SDL; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; public class GameScanner { // We use a singleton pattern to allow further optimizations @@ -145,7 +127,7 @@ private void scanGames(Activity activity){ private int scanFolderHash(Context context, Uri folderURI) { StringBuilder sb = new StringBuilder(); - for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { + for (String[] array : Helper.listChildrenDocuments(context, folderURI)) { sb.append(array[0]); sb.append(array[1]); } @@ -161,10 +143,10 @@ private void scanRootFolder(Activity activity, Uri folderURI) { final ArrayList fileURIs = new ArrayList<>(); // Precalculate how many folders are to be scanned - for (String[] array : Helper.listChildrenDocumentIDAndType(context, folderURI)) { + for (String[] array : Helper.listChildrenDocuments(context, folderURI)) { String fileDocumentID = array[0]; + String name = array[2]; - String name = Helper.getFileNameFromDocumentID(fileDocumentID); if (name.isEmpty()) { continue; } @@ -186,7 +168,7 @@ 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()); + Game[] candidates = findGames(fileURIs.get(i).toString(), names.get(i)); for (Game candidate: candidates) { if (candidate != null) { @@ -208,5 +190,5 @@ public boolean hasError() { return !errorList.isEmpty(); } - private static native Game[] findGames(String path); + private static native Game[] findGames(String path, String mainFolderName); } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java index 1fed838768..17baef7c35 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java @@ -7,7 +7,6 @@ import android.provider.DocumentsContract; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Button; import android.widget.LinearLayout; import android.widget.RadioButton; @@ -15,7 +14,6 @@ import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatSpinner; import androidx.documentfile.provider.DocumentFile; import org.easyrpg.player.Helper; @@ -90,13 +88,13 @@ private List scanAvailableSoundfonts(){ Uri soundFontsFolder = SettingsManager.getSoundFontsFolderURI(this); if (soundFontsFolder != null) { - for (String[] array : Helper.listChildrenDocumentIDAndType(this, soundFontsFolder)) { + for (String[] array : Helper.listChildrenDocuments(this, soundFontsFolder)) { String fileDocumentID = array[0]; String fileDocumentType = array[1]; + String name = array[2]; // Is it a soundfont file ? boolean isDirectory = Helper.isDirectoryFromMimeType(fileDocumentType); - String name = Helper.getFileNameFromDocumentID(fileDocumentID); if (!isDirectory && name.toLowerCase().endsWith(".sf2")) { DocumentFile soundFontFile = Helper.getFileFromDocumentID(this, soundFontsFolder, fileDocumentID); if (soundFontFile != null) { diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index 1e0f4d634e..fd72040c62 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -105,7 +105,7 @@ public static Set getFavoriteGamesList() { public static void addFavoriteGame(Game game) { // Update user's preferences - favoriteGamesList.add(game.getTitle()); + favoriteGamesList.add(game.getGameFolderPath()); setFavoriteGamesList(favoriteGamesList); } From 1e2bd02af1b57690e8b8f5b90f7d6be4578e3356 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 23 May 2024 21:17:03 +0200 Subject: [PATCH 13/26] Android: Try to load the title image in three ways 1. If only one title image: Use this one 2. By parsing the database 3. By taking the first image in the title folder This approach is surprisingly fast and the caching skips the scan after the first time. --- ...asyrpg_player_game_browser_GameScanner.cpp | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp index 1cf15c8f6b..92f0f27bb5 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp @@ -31,12 +31,15 @@ #include #include - #include "filefinder.h" #include "utils.h" #include "string_view.h" #include "platform/android/android.h" +#include +#include +#include + // via https://stackoverflow.com/q/1821806 static void custom_png_write_func(png_structp png_ptr, png_bytep data, png_size_t length) { std::vector *p = reinterpret_cast*>(png_get_io_ptr(png_ptr)); @@ -216,28 +219,86 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } } - // Very simple title graphic search: The first image in "Title" is used - auto title_fs = fs.Subtree("Title"); + /// Obtaining of the title image: + // 1. When the title directory contains only one image: Load it + // 2. Attempt to fetch it from the database + // 3. If this fails grab the first from the title folder jbyteArray title_image = nullptr; + + auto load_image = [&](Filesystem_Stream::InputStream& stream) { + if (!stream) { + return; + } + + if (stream.GetName().ends_with(".xyz")) { + title_image = readXyz(env, stream); + } else if (stream.GetName().ends_with(".png") || stream.GetName().ends_with(".bmp")) { + auto vec = Utils::ReadStream(stream); + title_image = env->NewByteArray(vec.size()); + env->SetByteArrayRegion(title_image, 0, vec.size(), reinterpret_cast(vec.data())); + } + }; + + // 1. When the title directory contains only one image: Load it + auto title_fs = fs.Subtree("Title"); if (title_fs) { - for (auto &[name, entry]: *title_fs.ListDirectory()) { - if (entry.type == DirectoryTree::FileType::Regular) { - if (StringView(name).ends_with(".xyz")) { - auto is = title_fs.OpenInputStream(entry.name); - title_image = readXyz(env, is); - } else if (StringView(name).ends_with(".png") || - StringView(name).ends_with(".bmp")) { - auto is = title_fs.OpenInputStream(entry.name); - if (!is) { - // When opening of the image fails it is an unsupported archive format - // Skip this game - continue; + auto& content = *title_fs.ListDirectory(); + if (content.size() == 1 && content[0].second.type == DirectoryTree::FileType::Regular) { + auto is = title_fs.OpenInputStream(content[0].second.name); + if (!is) { + // When opening of the image fails it is in an unsupported archive format + // Skip this game + continue; + } + load_image(is); + } + } + + // 2. Attempt to fetch it from the database + if (!title_image) { + std::string db_file = fs.FindFile("RPG_RT.ldb"); + if (!db_file.empty()) { + // This can fail when the database file is renamed, is not an error condition + auto is = fs.OpenInputStream(db_file); + if (!is) { + // When opening of the db fails it is in an unsupported archive format + // Skip this game + continue; + } else { + auto db = lcf::LDB_Reader::Load(is); + if (!db) { + // Database corrupted? Skip + continue; + } + + if (!db->system.title_name.empty()) { + auto encodings = lcf::ReaderUtil::DetectEncodings(*db); + for (auto &enc: encodings) { + lcf::Encoder encoder(enc); + if (encoder.IsOk()) { + std::string title_name = lcf::ToString(db->system.title_name); + encoder.Encode(title_name); + auto title_is = fs.OpenFile("Title", title_name, FileFinder::IMG_TYPES); + // Title image was found -> Load it + load_image(title_is); + } } + } + } + } + } - auto vec = Utils::ReadStream(is); - title_image = env->NewByteArray(vec.size()); - env->SetByteArrayRegion(title_image, 0, vec.size(), - reinterpret_cast(vec.data())); + // 3. Simply grab the first from the title folder + if (!title_image) { + // No image loaded yet: Grab the first from the title folder + if (title_fs) { + for (auto &[name, entry]: *title_fs.ListDirectory()) { + if (entry.type == DirectoryTree::FileType::Regular) { + auto is = title_fs.OpenInputStream(entry.name); + load_image(is); + if (title_image) { + break; + } } } } From f4c8c7a5e814f2937e78849dc1c1934f10177a6b Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 24 May 2024 15:41:36 +0200 Subject: [PATCH 14/26] Android: Fetch the game name from the INI The title is then reencoded based on the users encoding setting. --- ...asyrpg_player_game_browser_GameScanner.cpp | 123 ++++++++++++++---- ..._easyrpg_player_game_browser_GameScanner.h | 4 +- .../java/org/easyrpg/player/InitActivity.java | 2 +- .../org/easyrpg/player/game_browser/Game.java | 121 +++++++++++------ .../game_browser/GameBrowserActivity.java | 5 +- .../player/game_browser/GameScanner.java | 1 + .../player/settings/SettingsManager.java | 8 +- 7 files changed, 193 insertions(+), 71 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp index 92f0f27bb5..2b3f300e43 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.cpp @@ -39,6 +39,7 @@ #include #include #include +#include // via https://stackoverflow.com/q/1821806 static void custom_png_write_func(png_structp png_ptr, png_bytep data, png_size_t length) { @@ -164,15 +165,23 @@ jbyteArray readXyz(JNIEnv *env, std::istream& stream) { return result; } +std::string jstring_to_string(JNIEnv* env, jstring j_str) { + if (!j_str) { + return {}; + } + const char* chars = env->GetStringUTFChars(j_str, NULL); + std::string str(chars); + env->ReleaseStringUTFChars(j_str, chars); + return str; +} + extern "C" JNIEXPORT jobject JNICALL -Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring jpath, jstring jmain_dir_name) { +Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, jstring jpath, jstring jmain_dir_name) { EpAndroid::env = env; - const char* path = env->GetStringUTFChars(jpath, nullptr); - std::string spath(path); - env->ReleaseStringUTFChars(jpath, path); - + // jpath is the SAF path to the game, is converted to FilesystemView "root" + std::string spath = jstring_to_string(env, jpath); auto root = FileFinder::Root().Create(spath); std::vector fs_list = FileFinder::FindGames(root); @@ -180,10 +189,11 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass jobjectArray jgame_array = env->NewObjectArray(fs_list.size(), jgame_class, nullptr); if (fs_list.empty()) { + // No games found return jgame_array; } - jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V"); + jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); bool game_in_main_dir = false; if (fs_list.size() == 1) { @@ -196,21 +206,19 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass auto& fs = fs_list[i]; std::string full_path = FileFinder::GetFullFilesystemPath(fs); - std::string title; + std::string game_dir_name; if (game_in_main_dir) { // The main dir is URI encoded, the human readable name is in jmain_dir_name - const char* main_dir_name = env->GetStringUTFChars(jmain_dir_name, nullptr); - title = main_dir_name; - env->ReleaseStringUTFChars(jmain_dir_name, main_dir_name); + game_dir_name = jstring_to_string(env, jmain_dir_name); } else { // In all other cases the folder name is "clean" and can be used - title = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); + game_dir_name = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); } std::string save_path; if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { // Is an archive and needs a redirected save path - save_path = title; + save_path = game_dir_name; // compatibility with original GameScanner Java code (everything after the extension dot is removed) size_t ext = save_path.find('.'); @@ -219,10 +227,11 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } } - /// Obtaining of the title image: - // 1. When the title directory contains only one image: Load it + /* Obtaining of the game_dir_name image */ + + // 1. When the game_dir_name directory contains only one image: Load it // 2. Attempt to fetch it from the database - // 3. If this fails grab the first from the title folder + // 3. If this fails grab the first from the game_dir_name folder jbyteArray title_image = nullptr; auto load_image = [&](Filesystem_Stream::InputStream& stream) { @@ -239,7 +248,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } }; - // 1. When the title directory contains only one image: Load it + // 1. When the game_dir_name directory contains only one image: Load it auto title_fs = fs.Subtree("Title"); if (title_fs) { auto& content = *title_fs.ListDirectory(); @@ -274,8 +283,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass if (!db->system.title_name.empty()) { auto encodings = lcf::ReaderUtil::DetectEncodings(*db); for (auto &enc: encodings) { - lcf::Encoder encoder(enc); - if (encoder.IsOk()) { + if (lcf::Encoder encoder(enc); encoder.IsOk()) { std::string title_name = lcf::ToString(db->system.title_name); encoder.Encode(title_name); auto title_is = fs.OpenFile("Title", title_name, FileFinder::IMG_TYPES); @@ -288,9 +296,9 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } } - // 3. Simply grab the first from the title folder + // 3. Simply grab the first from the game_dir_name folder if (!title_image) { - // No image loaded yet: Grab the first from the title folder + // No image loaded yet: Grab the first from the game_dir_name folder if (title_fs) { for (auto &[name, entry]: *title_fs.ListDirectory()) { if (entry.type == DirectoryTree::FileType::Regular) { @@ -304,11 +312,39 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass } } - // Create an instance of "Game" + /* Setting the game title */ + // By default it is just the name of the directory + std::string title = game_dir_name; + bool title_from_ini = false; + + // Try to grab a title from the INI file + if (auto ini_is = fs.OpenFile("RPG_RT.ini"); ini_is) { + if (lcf::INIReader ini(ini_is); !ini.ParseError()) { + if (std::string ini_title = ini.GetString("RPG_RT", "GameTitle", ""); !ini_title.empty()) { + title = ini_title; + title_from_ini = true; + } + } + } + + /* Create an instance of "Game" */ jstring jgame_path = env->NewStringUTF(("content://" + full_path).c_str()); jstring jsave_path = env->NewStringUTF(save_path.c_str()); - jstring jtitle = env->NewStringUTF(title.c_str()); - jobject jgame_object = env->NewObject(jgame_class, jgame_constructor, jgame_path, jsave_path, jtitle, title_image); + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor, jgame_path, jsave_path, title_image); + + if (title_from_ini) { + // Store the raw string in the Game instance so it can be reencoded later via user setting + jbyteArray jtitle_raw = env->NewByteArray(title.size()); + env->SetByteArrayRegion(jtitle_raw, 0, title.size(), reinterpret_cast(title.data())); + jfieldID jtitle_raw_field = env->GetFieldID(jgame_class, "titleRaw", "[B"); + env->SetObjectField(jgame_object, jtitle_raw_field, jtitle_raw); + Java_org_easyrpg_player_game_1browser_Game_reencodeTitle(env, jgame_object); + } else { + // Use the folder name as the title + jstring jtitle = env->NewStringUTF(title.c_str()); + jmethodID jset_title_method = env->GetMethodID(jgame_class, "setTitle", "(Ljava/lang/String;)V"); + env->CallVoidMethod(jgame_object, jset_title_method, jtitle); + } env->SetObjectArrayElement(jgame_array, i, jgame_object); } @@ -317,3 +353,44 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass // This is sanitized on the Java site return jgame_array; } + +extern "C" +JNIEXPORT void JNICALL +Java_org_easyrpg_player_game_1browser_Game_reencodeTitle(JNIEnv *env, jobject thiz) { + jclass jgame_class = env->GetObjectClass(thiz); + + // Fetch the raw title string (result will be at the end in "title" variable) + jfieldID jtitle_raw_field = env->GetFieldID(jgame_class, "titleRaw", "[B"); + jbyteArray jtitle_raw = reinterpret_cast(env->GetObjectField(thiz, jtitle_raw_field)); + + if (!jtitle_raw) { + return; + } + + jbyte* title_data = env->GetByteArrayElements(jtitle_raw, NULL); + jsize title_len = env->GetArrayLength(jtitle_raw); + std::string title(reinterpret_cast(title_data), title_len); + env->ReleaseByteArrayElements(jtitle_raw, title_data, 0); + + // Obtain the encoding + jmethodID jget_encoding_method = env->GetMethodID(jgame_class, "getEncodingCode", "()Ljava/lang/String;"); + jstring jencoding = (jstring)env->CallObjectMethod(thiz, jget_encoding_method); + std::string encoding = jstring_to_string(env, jencoding); + if (encoding == "auto") { + lcf::Encoder enc(lcf::ReaderUtil::DetectEncoding(title)); + enc.Encode(title); + } else { + lcf::Encoder enc(encoding); + enc.Encode(title); + } + + if (title.empty()) { + // Something failed, do not set a new title + return; + } + + // Set the new title after reencoding + jstring jtitle = env->NewStringUTF(title.c_str()); + jmethodID jset_title_method = env->GetMethodID(jgame_class, "setTitle", "(Ljava/lang/String;)V"); + env->CallVoidMethod(thiz, jset_title_method, jtitle); +} diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h index 95e56ade0a..58290f9b65 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser_GameScanner.h @@ -8,10 +8,12 @@ extern "C" { #endif -extern "C" JNIEXPORT jobject JNICALL Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass clazz, jstring path, jstring jmain_dir_name); +JNIEXPORT void JNICALL +Java_org_easyrpg_player_game_1browser_Game_reencodeTitle(JNIEnv *env, jobject thiz); + #ifdef __cplusplus } #endif diff --git a/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java index 9a8d95202e..b3aab24ca5 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/InitActivity.java @@ -125,7 +125,7 @@ private void startGameStandalone() { String saveDir = getExternalFilesDir(null).getAbsolutePath() + "/Save"; new File(saveDir).mkdirs(); - Game project = new Game(gameDir, saveDir, "", null); + Game project = new Game(gameDir, saveDir, null); project.setStandalone(true); GameBrowserHelper.launchGame(this, project); finish(); 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 ff836836e4..764a3f7f36 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 @@ -18,6 +18,8 @@ public class Game implements Comparable { final static char escapeCode = '\u0001'; /** The title shown in the Game Browser */ private String title; + /** Bytes of the title string in an unspecified encoding */ + private byte[] titleRaw = null; /** Path to the game folder (forwarded via --project-path */ private final String gameFolderPath; /** Relative path to the save directory, made absolute by launchGame (forwarded via --save-path) */ @@ -29,7 +31,7 @@ public class Game implements Comparable { /** Game is launched from the APK via standalone mode */ private boolean standalone = false; - public Game(String gameFolderPath, String saveFolder, String title, byte[] titleScreen) { + public Game(String gameFolderPath, String saveFolder, byte[] titleScreen) { this.gameFolderPath = gameFolderPath; // is only relative here, launchGame will put this in the "saves" directory @@ -37,8 +39,6 @@ public Game(String gameFolderPath, String saveFolder, String title, byte[] title savePath = saveFolder; } - this.title = title; - if (titleScreen != null) { this.titleScreen = BitmapFactory.decodeByteArray(titleScreen, 0, titleScreen.length); }; @@ -46,33 +46,16 @@ public Game(String gameFolderPath, String saveFolder, String title, byte[] title this.isFavorite = isFavoriteFromSettings(); } - public static Game fromCacheEntry(Context context, String cache) { - String[] entries = cache.split(String.valueOf(escapeCode)); - - if (entries.length != 4) { - return null; - } - - String savePath = entries[0]; - DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[1])); - if (gameFolder == null) { - return null; - } - - String title = entries[2]; - - byte[] titleScreen = null; - if (!entries[3].equals("null")) { - titleScreen = Base64.decode(entries[3], 0); - } - - return new Game(entries[1], savePath, title, titleScreen); - } - public String getTitle() { return title; } + public void setTitle(String title) { + this.title = title; + } + + public native void reencodeTitle(); + public String getGameFolderPath() { return gameFolderPath; } @@ -113,12 +96,41 @@ public int compareTo(Game game) { return this.title.compareTo(game.title); } + /** + * Returns a unique key to be used for storing settings related to the game. + * + * @return unique key + */ + public String getKey() { + return gameFolderPath.replaceAll("[/ ]", "_"); + } + public Encoding getEncoding() { return SettingsManager.getGameEncoding(this); } public void setEncoding(Encoding encoding) { SettingsManager.setGameEncoding(this, encoding); + reencodeTitle(); + } + + /** + * @return The encoding number or "auto" when not configured (for use via JNI) + */ + public String getEncodingCode() { + return getEncoding().getRegionCode(); + } + + public Bitmap getTitleScreen() { + return titleScreen; + } + + public boolean isStandalone() { + return standalone; + } + + public void setStandalone(boolean standalone) { + this.standalone = standalone; } @NonNull @@ -127,17 +139,58 @@ public String toString() { return getTitle(); } + public static Game fromCacheEntry(Context context, String cache) { + String[] entries = cache.split(String.valueOf(escapeCode)); + + if (entries.length != 5) { + return null; + } + + String savePath = entries[0]; + DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[1])); + if (gameFolder == null) { + return null; + } + + String title = entries[2]; + + byte[] titleRaw = null; + if (!entries[3].equals("null")) { + titleRaw = Base64.decode(entries[3], 0); + } + + byte[] titleScreen = null; + if (!entries[4].equals("null")) { + titleScreen = Base64.decode(entries[4], 0); + } + + Game g = new Game(entries[1], savePath, titleScreen); + g.setTitle(title); + g.titleRaw = titleRaw; + + if (g.titleRaw != null) { + g.reencodeTitle(); + } + + return g; + } + public String toCacheEntry() { StringBuilder sb = new StringBuilder(); - // Cache structure: savePath | gameFolderPath | title | titleScreen + // Cache structure: savePath | gameFolderPath | title | titleRaw | titleScreen sb.append(savePath); sb.append(escapeCode); sb.append(gameFolderPath); sb.append(escapeCode); sb.append(title); sb.append(escapeCode); - + if (titleRaw != null) { + sb.append(Base64.encodeToString(titleRaw, Base64.NO_WRAP)); + } else { + sb.append("null"); + } + sb.append(escapeCode); if (titleScreen != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); titleScreen.compress(Bitmap.CompressFormat.PNG, 90, baos); @@ -149,16 +202,4 @@ public String toCacheEntry() { return sb.toString(); } - - public Bitmap getTitleScreen() { - return titleScreen; - } - - public boolean isStandalone() { - return standalone; - } - - public void setStandalone(boolean standalone) { - this.standalone = standalone; - } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java index 5e55c47521..791dcf3c18 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java @@ -321,7 +321,7 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { .setTitle(R.string.settings) .setItems(choices_list, (dialog, which) -> { if (which == 0) { - chooseRegion(activity, gameList.get(position)); + chooseRegion(activity, holder, gameList.get(position)); } else if (which == 1) { launchGame(position, true); } @@ -365,7 +365,7 @@ public void updateFavoriteButton(ViewHolder holder, Game game){ holder.favoriteButton.setImageResource(buttonImageResource); } - public void chooseRegion(final Context context, final Game game) { + public void chooseRegion(final Context context, final ViewHolder holder, final Game game) { // The list of region choices String[] region_array = Encoding.getEncodingDescriptions(context); @@ -383,6 +383,7 @@ public void chooseRegion(final Context context, final Game game) { if (!selectedEncoding.equals(encoding)) { game.setEncoding(selectedEncoding); + holder.title.setText(game.getTitle()); } }) .setNegativeButton(R.string.cancel, 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 628089d841..7fddba2362 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 @@ -127,6 +127,7 @@ private void scanGames(Activity activity){ private int scanFolderHash(Context context, Uri folderURI) { StringBuilder sb = new StringBuilder(); + sb.append("2"); // Bump this when the cache layout changes for (String[] array : Helper.listChildrenDocuments(context, folderURI)) { sb.append(array[0]); sb.append(array[1]); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index fd72040c62..2d64113f01 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -105,13 +105,13 @@ public static Set getFavoriteGamesList() { public static void addFavoriteGame(Game game) { // Update user's preferences - favoriteGamesList.add(game.getGameFolderPath()); + favoriteGamesList.add(game.getKey()); setFavoriteGamesList(favoriteGamesList); } public static void removeAFavoriteGame(Game game) { - favoriteGamesList.remove(game.getTitle()); + favoriteGamesList.remove(game.getKey()); setFavoriteGamesList(favoriteGamesList); } @@ -383,11 +383,11 @@ public static void setInputLayoutVertical(Activity activity, InputLayout inputLa } public static Encoding getGameEncoding(Game game) { - return Encoding.regionCodeToEnum(pref.getString(game.getTitle() + "_Encoding", "")); + return Encoding.regionCodeToEnum(pref.getString(game.getKey() + "_Encoding", "")); } public static void setGameEncoding(Game game, Encoding encoding) { - editor.putString(game.getTitle() + "_Encoding", encoding.getRegionCode()); + editor.putString(game.getKey() + "_Encoding", encoding.getRegionCode()); editor.commit(); } From 7b5ad5bc65681f9612cd3c7084afe0ffd6e23208 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 24 May 2024 16:01:17 +0200 Subject: [PATCH 15/26] Android: Add option to rename the game --- .../org/easyrpg/player/game_browser/Game.java | 17 +++++++++-- .../game_browser/GameBrowserActivity.java | 28 +++++++++++++++++++ .../player/settings/SettingsManager.java | 10 ++++++- .../app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 54 insertions(+), 3 deletions(-) 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 764a3f7f36..fb2286582c 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 @@ -47,6 +47,11 @@ public Game(String gameFolderPath, String saveFolder, byte[] titleScreen) { } public String getTitle() { + String customTitle = getCustomTitle(); + if (!customTitle.isEmpty()) { + return customTitle; + } + return title; } @@ -56,6 +61,14 @@ public void setTitle(String title) { public native void reencodeTitle(); + public String getCustomTitle() { + return SettingsManager.getCustomGameTitle(this); + } + + public void setCustomTitle(String customTitle) { + SettingsManager.setCustomGameTitle(this, customTitle); + } + public String getGameFolderPath() { return gameFolderPath; } @@ -82,7 +95,7 @@ public void setFavorite(boolean isFavorite) { } private boolean isFavoriteFromSettings() { - return SettingsManager.getFavoriteGamesList().contains(this.gameFolderPath); + return SettingsManager.getFavoriteGamesList().contains(this.getKey()); } @Override @@ -93,7 +106,7 @@ public int compareTo(Game game) { if (!this.isFavorite() && game.isFavorite()) { return 1; } - return this.title.compareTo(game.title); + return this.getTitle().compareTo(game.getTitle()); } /** diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java index 791dcf3c18..5b622b3141 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java @@ -8,6 +8,7 @@ import android.net.Uri; import android.os.Bundle; import android.provider.DocumentsContract; +import android.text.InputType; import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; @@ -16,6 +17,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RelativeLayout; @@ -313,6 +315,7 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { holder.settingsButton.setOnClickListener(v -> { String[] choices_list = { activity.getResources().getString(R.string.select_game_region), + activity.getResources().getString(R.string.game_rename), activity.getResources().getString(R.string.launch_debug_mode) }; @@ -323,6 +326,8 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { if (which == 0) { chooseRegion(activity, holder, gameList.get(position)); } else if (which == 1) { + renameGame(activity, holder, gameList.get(position)); + } else if (which == 2) { launchGame(position, true); } }); @@ -390,6 +395,29 @@ public void chooseRegion(final Context context, final ViewHolder holder, final G builder.show(); } + public void renameGame(final Context context, final ViewHolder holder, final Game game) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + + // Set up text input + final EditText input = new EditText(context); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setText(holder.title.getText()); + builder.setView(input); + + builder + .setTitle(R.string.game_rename) + .setPositiveButton(R.string.ok, (dialog, id) -> { + game.setCustomTitle(input.getText().toString()); + holder.title.setText(game.getTitle()); + }) + .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.revert, (dialog, id) -> { + game.setCustomTitle(""); + holder.title.setText(game.getTitle()); + }); + builder.show(); + } + public static class ViewHolder extends RecyclerView.ViewHolder { public TextView title; public ImageView titleScreen; diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index 2d64113f01..b444f79791 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -106,7 +106,6 @@ public static Set getFavoriteGamesList() { public static void addFavoriteGame(Game game) { // Update user's preferences favoriteGamesList.add(game.getKey()); - setFavoriteGamesList(favoriteGamesList); } @@ -391,6 +390,15 @@ public static void setGameEncoding(Game game, Encoding encoding) { editor.commit(); } + public static String getCustomGameTitle(Game game) { + return pref.getString(game.getKey() + "_Title", ""); + } + + public static void setCustomGameTitle(Game game, String customTitle) { + editor.putString(game.getKey() + "_Title", customTitle); + editor.commit(); + } + public static int getSpeedModifierA() { return speedModifierA; } diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index 6d92b8777c..b7467f943b 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ Do you really want to quit? Yes No + Revert Creating $PATH directory failed $PATH not readable No RPG Maker 2000/2003 games found.\n\nThe EasyRPG folder you selected contains a \"games\" folder. Use a file manager app to put your games in this folder.\nGames can be put in subfolders or in ZIP/LZH archives.\n\nThe EasyRPG folder can be changed in the settings. @@ -31,6 +32,7 @@ Select game region Change the layout Launch in debug mode + Rename game Choose a layout Unknown region Changing region failed From f08e92b0845d2399412fb51e575542a024fa933d Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 24 May 2024 16:50:37 +0200 Subject: [PATCH 16/26] Android: Provide soundfont path via command line. Fix typos --- .../game_browser/GameBrowserHelper.java | 9 ++++++++- .../settings/SettingsAudioActivity.java | 4 ++-- .../player/settings/SettingsManager.java | 20 +++++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index e8f20f56ca..72050eaf77 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -79,12 +79,19 @@ public static void launchGame(Context context, Game game, boolean debugMode) { args.add(context.getExternalFilesDir(null).getAbsolutePath()); // Soundfont - Uri soundfontUri = SettingsManager.getSoundFountFileURI(context); + Uri soundfontUri = SettingsManager.getSoundFontFileURI(context); if (soundfontUri != null) { args.add("--soundfont"); args.add(soundfontUri.toString()); } + // Sound Font Folder path (used by the settings scene) + Uri soundFontFolderUri = SettingsManager.getSoundFontsFolderURI(context); + if (soundFontFolderUri != null) { + args.add("--soundfont-path"); + args.add(soundFontFolderUri.toString()); + } + if (debugMode) { args.add("--test-play"); } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java index 17baef7c35..94a1c5965d 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java @@ -111,7 +111,7 @@ public SoundfontItemList getDefaultSoundfont(Context context) { } public static boolean isSelectedSoundfontFile(Context context, Uri soundfontUri) { - Uri selectedSoundFontUri = SettingsManager.getSoundFountFileURI(context); + Uri selectedSoundFontUri = SettingsManager.getSoundFontFileURI(context); if (soundfontUri == null && selectedSoundFontUri == null) { return true; } @@ -145,7 +145,7 @@ public SoundfontItemList(Context context, String name, Uri uri) { } public void select() { - SettingsManager.setSoundFountFileURI(uri); + SettingsManager.setSoundFontFileURI(uri); // Uncheck other RadioButton for (SoundfontItemList s : soundfontList) { diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index b444f79791..e8d43b38b2 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -40,7 +40,7 @@ public class SettingsManager { private static int speedModifierA; private static InputLayout inputLayoutHorizontal, inputLayoutVertical; // Note: don't store DocumentFile as they can be nullify with a change of context - private static Uri easyRPGFolderURI, soundFountFileURI; + private static Uri easyRPGFolderURI, soundFontFileURI; private static Set favoriteGamesList = new HashSet<>(); private static int gamesCacheHash; private static Set gamesCache = new HashSet<>(); @@ -324,24 +324,24 @@ public static Uri getSoundFontsFolderURI(Context context) { } } - public static Uri getSoundFountFileURI(Context context) { - if (soundFountFileURI == null) { + public static Uri getSoundFontFileURI(Context context) { + if (soundFontFileURI == null) { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); String soundfontURI = sharedPref.getString(SettingsEnum.SOUNDFONT_URI.toString(), ""); if (soundfontURI.isEmpty()) { - soundFountFileURI = null; + soundFontFileURI = null; } else { - soundFountFileURI = Uri.parse(soundfontURI); + soundFontFileURI = Uri.parse(soundfontURI); } } - return soundFountFileURI; + return soundFontFileURI; } - public static void setSoundFountFileURI(Uri soundFountFileURI) { + public static void setSoundFontFileURI(Uri soundFontFileURI) { String st = ""; - SettingsManager.soundFountFileURI = soundFountFileURI; - if (soundFountFileURI != null) { - st = soundFountFileURI.toString(); + SettingsManager.soundFontFileURI = soundFontFileURI; + if (soundFontFileURI != null) { + st = soundFontFileURI.toString(); } editor.putString(SettingsEnum.SOUNDFONT_URI.toString(), st); editor.commit(); From 555db42c96889abd33cbd66deac0253261a3a336 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 24 May 2024 18:16:29 +0200 Subject: [PATCH 17/26] Android: Make it possible to configure the Font Also minor Soundfont code fixes --- .../android/app/src/main/AndroidManifest.xml | 5 + .../main/java/org/easyrpg/player/Helper.java | 1 + .../game_browser/GameBrowserHelper.java | 16 +- .../org/easyrpg/player/settings/IniFile.java | 2 +- .../settings/SettingsAudioActivity.java | 4 +- .../easyrpg/player/settings/SettingsEnum.java | 9 +- .../player/settings/SettingsFontActivity.java | 268 ++++++++++++++++++ .../player/settings/SettingsMainActivity.java | 4 + .../player/settings/SettingsManager.java | 96 ++++++- .../res/layout/activity_settings_font.xml | 128 +++++++++ .../res/layout/activity_settings_main.xml | 8 + .../app/src/main/res/values/strings.xml | 9 +- 12 files changed, 529 insertions(+), 21 deletions(-) create mode 100644 builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java create mode 100644 builds/android/app/src/main/res/layout/activity_settings_font.xml diff --git a/builds/android/app/src/main/AndroidManifest.xml b/builds/android/app/src/main/AndroidManifest.xml index 8776251166..f1a8a6f80d 100644 --- a/builds/android/app/src/main/AndroidManifest.xml +++ b/builds/android/app/src/main/AndroidManifest.xml @@ -99,6 +99,11 @@ android:label="@string/input" android:parentActivityName=".settings.SettingsMainActivity"> + + diff --git a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java index 7a49df8e9d..5772805b89 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java @@ -111,6 +111,7 @@ public static void createEasyRPGFolders(Context context, Uri easyRPGFolderURI){ createFolder(context, easyRPGFolder, SettingsManager.GAMES_FOLDER_NAME); createFolder(context, easyRPGFolder, SettingsManager.SOUND_FONTS_FOLDER_NAME); createFolder(context, easyRPGFolder, SettingsManager.SAVES_FOLDER_NAME); + createFolder(context, easyRPGFolder, SettingsManager.FONTS_FOLDER_NAME); // The .nomedia file (avoid media app to scan games and RTP folders) if (Helper.findFile(context, easyRPGFolder.getUri(), ".nomedia") == null) { diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index 72050eaf77..dcb6d32460 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -78,13 +78,7 @@ public static void launchGame(Context context, Game game, boolean debugMode) { args.add("--config-path"); args.add(context.getExternalFilesDir(null).getAbsolutePath()); - // Soundfont - Uri soundfontUri = SettingsManager.getSoundFontFileURI(context); - if (soundfontUri != null) { - args.add("--soundfont"); - args.add(soundfontUri.toString()); - } - + /* FIXME: Currently disabled because the built-in scene cannot handle URI-encoded paths // Sound Font Folder path (used by the settings scene) Uri soundFontFolderUri = SettingsManager.getSoundFontsFolderURI(context); if (soundFontFolderUri != null) { @@ -92,6 +86,14 @@ public static void launchGame(Context context, Game game, boolean debugMode) { args.add(soundFontFolderUri.toString()); } + // Font Folder path (used by the settings scene) + Uri fontFolderUri = SettingsManager.getFontsFolderURI(context); + if (fontFolderUri != null) { + args.add("--font-path"); + args.add(fontFolderUri.toString()); + } + */ + if (debugMode) { args.add("--test-play"); } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java index 35648fdcb3..1220dd87bd 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/IniFile.java @@ -30,7 +30,7 @@ public IniFile(File iniFile) { video = new SectionView("Video"); audio = new SectionView("Audio"); input = new SectionView("Input"); - engine = new SectionView("Engine"); + engine = new SectionView("Player"); } public boolean save() { diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java index 94a1c5965d..6a84265184 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java @@ -95,7 +95,7 @@ private List scanAvailableSoundfonts(){ // Is it a soundfont file ? boolean isDirectory = Helper.isDirectoryFromMimeType(fileDocumentType); - if (!isDirectory && name.toLowerCase().endsWith(".sf2")) { + if (!isDirectory && (name.toLowerCase().endsWith(".sf2") || name.toLowerCase().endsWith(".soundfont"))) { DocumentFile soundFontFile = Helper.getFileFromDocumentID(this, soundFontsFolder, fileDocumentID); if (soundFontFile != null) { soundfontList.add(new SoundfontItemList(this, name, soundFontFile.getUri())); @@ -111,7 +111,7 @@ public SoundfontItemList getDefaultSoundfont(Context context) { } public static boolean isSelectedSoundfontFile(Context context, Uri soundfontUri) { - Uri selectedSoundFontUri = SettingsManager.getSoundFontFileURI(context); + Uri selectedSoundFontUri = SettingsManager.getSoundFontFileURI(); if (soundfontUri == null && selectedSoundFontUri == null) { return true; } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java index d86951835d..d06a03c0ba 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsEnum.java @@ -11,7 +11,7 @@ enum SettingsEnum { LAYOUT_SIZE("PREF_SIZE_EVERY_BUTTONS"), EASYRPG_FOLDER_URI("PREF_EASYRPG_FOLDER_URI"), ENABLE_RTP_SCANNING("PREF_ENABLE_RTP_SCANNING"), - SOUNDFONT_URI("PREF_SOUNDFONT_URI"), + SOUNDFONT_URI("Soundfont"), FAVORITE_GAMES("PREF_FAVORITE_GAMES_NEW"), CACHE_GAMES_HASH("PREF_CACHE_GAMES_HASH"), CACHE_GAMES("PREF_CACHE_GAMES"), @@ -23,7 +23,12 @@ enum SettingsEnum { SOUND_VOLUME("SoundVolume"), STRETCH("Stretch"), GAME_RESOLUTION("GameResolution"), - SPEED_MODIFIER_A("SpeedModifierA") + SPEED_MODIFIER_A("SpeedModifierA"), + FONT1_URI("Font1"), + FONT2_URI("Font2"), + FONT1_SIZE("Font1Size"), + FONT2_SIZE("Font2Size") + ; diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java new file mode 100644 index 0000000000..83ab8a87db --- /dev/null +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java @@ -0,0 +1,268 @@ +package org.easyrpg.player.settings; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.DocumentsContract; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.documentfile.provider.DocumentFile; + +import org.easyrpg.player.Helper; +import org.easyrpg.player.R; + +import java.util.ArrayList; +import java.util.List; + +public class SettingsFontActivity extends AppCompatActivity { + private LinearLayout fonts1ListLayout; + private LinearLayout fonts2ListLayout; + private String[] extensions = new String[] {".fon", ".fnt", ".bdf", ".ttf", ".ttc", ".otf", ".woff2", ".woff"}; + + List font1List; + List font2List; + private final int SIZE_MIN = 6; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings_font); + + fonts1ListLayout = findViewById(R.id.settings_font1_list); + fonts2ListLayout = findViewById(R.id.settings_font2_list); + + SettingsManager.init(getApplicationContext()); + + // Setup UI components + // The Font Button + Button button = this.findViewById(R.id.button_open_font_folder); + // We can open the file picker in a specific folder only with API >= 26 + if (android.os.Build.VERSION.SDK_INT >= 26) { + button.setOnClickListener(v -> { + // Open the file explorer in the "fonts" folder + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SettingsManager.getFontsFolderURI(this)); + startActivity(intent); + }); + } else { + ViewGroup layout = (ViewGroup) button.getParent(); + if(layout != null) { + layout.removeView(button); + } + } + + configureFont1Size(); + configureFont2Size(); + } + + @Override + protected void onResume() { + super.onResume(); + + updateFontsListView(); + } + + private void updateFontsListView() { + fonts1ListLayout.removeAllViews(); + fonts2ListLayout.removeAllViews(); + + boolean font1Selected = false; + boolean font2Selected = false; + + scanAvailableFonts(); + for (FontItemList i : font1List) { + fonts1ListLayout.addView(i.getRadioButton()); + if (i.isSelected()) { + font1Selected = true; + } + } + for (FontItemList i : font2List) { + fonts2ListLayout.addView(i.getRadioButton()); + if (i.isSelected()) { + font2Selected = true; + } + } + + // If no font is selected, select the default one + if (!font1Selected) { + font1List.get(0).setSelected(true); + } + if (!font2Selected) { + font2List.get(0).setSelected(true); + } + } + + private void scanAvailableFonts(){ + font1List = new ArrayList<>(); + font2List = new ArrayList<>(); + font1List.add(new FontItemList(this, this.getString(R.string.settings_font_default), null, true)); + font2List.add(new FontItemList(this, this.getString(R.string.settings_font_default), null, false)); + + Uri fontsFolder = SettingsManager.getFontsFolderURI(this); + if (fontsFolder != null) { + for (String[] array : Helper.listChildrenDocuments(this, fontsFolder)) { + String fileDocumentID = array[0]; + String fileDocumentType = array[1]; + String name = array[2]; + + // Is it a font file ? + boolean isDirectory = Helper.isDirectoryFromMimeType(fileDocumentType); + String lname = name.toLowerCase(); + boolean fontOk = false; + for (String ext: extensions) { + if (lname.endsWith(ext)) { + fontOk = true; + break; + } + } + if (!isDirectory && fontOk) { + DocumentFile fontFile = Helper.getFileFromDocumentID(this, fontsFolder, fileDocumentID); + if (fontFile != null) { + font1List.add(new FontItemList(this, name, fontFile.getUri(), true)); + font2List.add(new FontItemList(this, name, fontFile.getUri(), false)); + } + } + } + } + } + + public static boolean isSelectedFontFile(Context context, Uri fontUri, boolean firstFont) { + Uri selectedFontUri = null; + if (firstFont) { + selectedFontUri = SettingsManager.getFont1FileURI(); + } else { + selectedFontUri = SettingsManager.getFont2FileURI(); + } + if (fontUri == null && selectedFontUri == null) { + return true; + } + else if (fontUri != null) { + return fontUri.equals(selectedFontUri); + } else { + return false; + } + } + + class FontItemList { + private final String name; + private final Uri uri; + private final RadioButton radioButton; + private final boolean firstFont; + + public FontItemList(Context context, String name, Uri uri, boolean firstFont) { + this.name = name; + this.uri = uri; + this.firstFont = firstFont; + + // The Radio Button + View layout = getLayoutInflater().inflate(R.layout.settings_soundfont_item_list, null); + this.radioButton = layout.findViewById(R.id.settings_soundfont_radio_button); + radioButton.setOnClickListener(v -> select()); + if (isSelectedFontFile(context, uri, firstFont)) { + setSelected(true); + } + + // The name + radioButton.setText(name); + radioButton.setOnClickListener(v -> select()); + } + + public void select() { + if (firstFont) { + SettingsManager.setFont1FileURI(uri); + for (FontItemList s : font1List) { + s.getRadioButton().setChecked(false); + } + radioButton.setChecked(true); + } else { + SettingsManager.setFont2FileURI(uri); + for (FontItemList s : font2List) { + s.getRadioButton().setChecked(false); + } + radioButton.setChecked(true); + } + } + + public String getName() { + return name; + } + + public Uri getUri() { + return uri; + } + + public boolean isSelected() { + return radioButton.isChecked(); + } + + public void setSelected(boolean selected) { + radioButton.setChecked(selected); + } + + public RadioButton getRadioButton() { + return radioButton; + } + } + + private void configureFont1Size() { + SeekBar font1SizeSeekBar = findViewById(R.id.settings_font1_size); + font1SizeSeekBar.setProgress(SettingsManager.getFont1Size() - SIZE_MIN); + + TextView t = findViewById(R.id.settings_font1_size_text_view); + + font1SizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + SettingsManager.setFont1Size(seekBar.getProgress() + SIZE_MIN); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + t.setText(String.valueOf(font1SizeSeekBar.getProgress() + SIZE_MIN)); + } + }); + + t.setText(String.valueOf(font1SizeSeekBar.getProgress() + SIZE_MIN)); + } + + private void configureFont2Size() { + SeekBar fontSize2SeekBar = findViewById(R.id.settings_font2_size); + fontSize2SeekBar.setProgress(SettingsManager.getFont2Size() - SIZE_MIN); + + TextView t = findViewById(R.id.settings_font2_size_text_view); + + fontSize2SeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + SettingsManager.setFont2Size(seekBar.getProgress() + SIZE_MIN); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + t.setText(String.valueOf(fontSize2SeekBar.getProgress() + SIZE_MIN)); + } + }); + + t.setText(String.valueOf(fontSize2SeekBar.getProgress() + SIZE_MIN)); + } +} + diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsMainActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsMainActivity.java index ae94fcbfb3..b13979b2f5 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsMainActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsMainActivity.java @@ -24,6 +24,8 @@ public void onCreate(Bundle savedInstanceState) { audioButton.setOnClickListener(this); Button inputsButton = findViewById(R.id.settings_main_input); inputsButton.setOnClickListener(this); + Button fontButton = findViewById(R.id.settings_main_font); + fontButton.setOnClickListener(this); Button folderButton = findViewById(R.id.settings_main_easyrpg_folders); folderButton.setOnClickListener(this); } @@ -41,6 +43,8 @@ public void onClick(View v) { intent = new Intent(this, SettingsGamesFolderActivity.class); } else if (id == R.id.settings_main_input) { intent = new Intent(this, SettingsInputActivity.class); + } else if (id == R.id.settings_main_font) { + intent = new Intent(this, SettingsFontActivity.class); } if (intent != null) { diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java index e8d43b38b2..0c414d7ba1 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsManager.java @@ -40,13 +40,15 @@ public class SettingsManager { private static int speedModifierA; private static InputLayout inputLayoutHorizontal, inputLayoutVertical; // Note: don't store DocumentFile as they can be nullify with a change of context - private static Uri easyRPGFolderURI, soundFontFileURI; + private static Uri easyRPGFolderURI, soundFontFileURI, font1FileURI, font2FileURI; + private static int font1Size, font2Size; private static Set favoriteGamesList = new HashSet<>(); private static int gamesCacheHash; private static Set gamesCache = new HashSet<>(); public static String RTP_FOLDER_NAME = "rtp", RTP_2000_FOLDER_NAME = "2000", RTP_2003_FOLDER_NAME = "2003", SOUND_FONTS_FOLDER_NAME = "soundfonts", - GAMES_FOLDER_NAME = "games", SAVES_FOLDER_NAME = "saves"; + GAMES_FOLDER_NAME = "games", SAVES_FOLDER_NAME = "saves", + FONTS_FOLDER_NAME = "fonts"; public static int FAST_FORWARD_MODE_HOLD = 0, FAST_FORWARD_MODE_TAP = 1; private static List imageSizeOption = Arrays.asList("nearest", "integer", "bilinear"); @@ -81,6 +83,9 @@ private static void loadSettings(Context context) { musicVolume = configIni.audio.getInteger(MUSIC_VOLUME.toString(), 100); soundVolume = configIni.audio.getInteger(SOUND_VOLUME.toString(), 100); + font1Size = configIni.engine.getInteger(FONT1_SIZE.toString(), 12); + font2Size = configIni.engine.getInteger(FONT2_SIZE.toString(), 12); + speedModifierA = configIni.input.getInteger(SPEED_MODIFIER_A.toString(), 3); favoriteGamesList = new HashSet<>(sharedPref.getStringSet(FAVORITE_GAMES.toString(), new HashSet<>())); @@ -315,6 +320,15 @@ public static Uri getRTPFolderURI(Context context) { } } + public static Uri getFontsFolderURI(Context context) { + DocumentFile easyRPGFolder = Helper.getFileFromURI(context, easyRPGFolderURI); + if (easyRPGFolder != null) { + return Helper.findFileUri(context, easyRPGFolder.getUri(), FONTS_FOLDER_NAME); + } else { + return null; + } + } + public static Uri getSoundFontsFolderURI(Context context) { DocumentFile easyRPGFolder = Helper.getFileFromURI(context, easyRPGFolderURI); if (easyRPGFolder != null) { @@ -324,10 +338,29 @@ public static Uri getSoundFontsFolderURI(Context context) { } } - public static Uri getSoundFontFileURI(Context context) { + public static int getFont1Size() { + return font1Size; + } + + public static void setFont1Size(int i) { + font1Size = i; + configIni.engine.set(FONT1_SIZE.toString(), i); + configIni.save(); + } + + public static int getFont2Size() { + return font2Size; + } + + public static void setFont2Size(int i) { + font2Size = i; + configIni.engine.set(FONT2_SIZE.toString(), i); + configIni.save(); + } + + public static Uri getSoundFontFileURI() { if (soundFontFileURI == null) { - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); - String soundfontURI = sharedPref.getString(SettingsEnum.SOUNDFONT_URI.toString(), ""); + String soundfontURI = configIni.audio.getString(SOUNDFONT_URI.toString(), ""); if (soundfontURI.isEmpty()) { soundFontFileURI = null; } else { @@ -341,10 +374,57 @@ public static void setSoundFontFileURI(Uri soundFontFileURI) { String st = ""; SettingsManager.soundFontFileURI = soundFontFileURI; if (soundFontFileURI != null) { - st = soundFontFileURI.toString(); + configIni.audio.set(SOUNDFONT_URI.toString(), soundFontFileURI.toString()); + } else { + configIni.audio.set(SOUNDFONT_URI.toString(), ""); } - editor.putString(SettingsEnum.SOUNDFONT_URI.toString(), st); - editor.commit(); + configIni.save(); + } + + public static Uri getFont1FileURI() { + if (font1FileURI == null) { + String fontURI = configIni.engine.getString(FONT1_URI.toString(), ""); + if (fontURI.isEmpty()) { + font1FileURI = null; + } else { + font1FileURI = Uri.parse(fontURI); + } + } + return font1FileURI; + } + + public static void setFont1FileURI(Uri fontFileURI) { + String st = ""; + SettingsManager.font1FileURI = fontFileURI; + if (fontFileURI != null) { + configIni.engine.set(FONT1_URI.toString(), fontFileURI.toString()); + } else { + configIni.engine.set(FONT1_URI.toString(), ""); + } + configIni.save(); + } + + public static Uri getFont2FileURI() { + if (font2FileURI == null) { + String fontURI = configIni.engine.getString(FONT2_URI.toString(), ""); + if (fontURI.isEmpty()) { + font2FileURI = null; + } else { + font2FileURI = Uri.parse(fontURI); + } + } + return font2FileURI; + } + + public static void setFont2FileURI(Uri fontFileURI) { + String st = ""; + SettingsManager.font2FileURI = fontFileURI; + if (fontFileURI != null) { + configIni.engine.set(FONT2_URI.toString(), fontFileURI.toString()); + } else { + configIni.engine.set(FONT2_URI.toString(), ""); + } + configIni.save(); } public static InputLayout getInputLayoutHorizontal(Activity activity) { diff --git a/builds/android/app/src/main/res/layout/activity_settings_font.xml b/builds/android/app/src/main/res/layout/activity_settings_font.xml new file mode 100644 index 0000000000..95d45c587f --- /dev/null +++ b/builds/android/app/src/main/res/layout/activity_settings_font.xml @@ -0,0 +1,128 @@ + + + + + +