From d01908f968bc94906ebc74c4d496e788763694b1 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 00:12:15 +0100 Subject: [PATCH 01/18] Add detecting project type of the game --- src/filefinder.cpp | 29 +++++++++++++++++++++++++++++ src/filefinder.h | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 52e1d4244f..74d427c397 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -312,6 +312,35 @@ bool FileFinder::IsRPG2kProjectWithRenames(const FilesystemView& fs) { return !FileExtGuesser::GetRPG2kProjectWithRenames(fs).Empty(); } +FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { + if (IsValidProject(fs)) { + return FileFinder::ProjectType::Supported; + } + + // TODO: Find remaining RGSS dlls + if (!fs.FindFile("RGSS104E.dll").empty()) { + return FileFinder::ProjectType::RpgMakerXp; + } + + if (!fs.FindFile("RGSS202E.dll").empty()) { + return FileFinder::ProjectType::RpgMakerVx; + } + + if (!fs.FindFile("System", "RGSS301.dll").empty()) { + return FileFinder::ProjectType::RpgMakerVxAce; + } + + if (!fs.FindFile("nw.dll").empty()) { + return FileFinder::ProjectType::RpgMakerMvMz; + } + + if (!fs.FindFile("GuruGuruSMF4.dll").empty()) { + return FileFinder::ProjectType::WolfRpgEditor; + } + + return FileFinder::ProjectType::Unknown; +} + bool FileFinder::OpenViewToEasyRpgFile(FilesystemView& fs) { auto files = fs.ListDirectory(); if (!files) { diff --git a/src/filefinder.h b/src/filefinder.h index 0aa5eccdae..3bf844e624 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -45,6 +45,30 @@ namespace FileFinder { constexpr const auto FONTS_TYPES = Utils::MakeSvArray(".fon", ".fnt", ".bdf", ".ttf", ".ttc", ".otf", ".woff2", ".woff"); constexpr const auto TEXT_TYPES = Utils::MakeSvArray(".txt", ".csv", ""); // "" = Complete Filename (incl. extension) provided by the user + /** + * Type of the project. Used to differentiate between supported games (2kX or EasyRPG) + * and known but unsupported (i.e. newer RPG Makers). + */ + enum ProjectType { + Unknown, + // 2kX or EasyRPG + Supported, + // Known unsupported engines + RpgMakerXp, + RpgMakerVx, + RpgMakerVxAce, + RpgMakerMvMz, + WolfRpgEditor, + }; + + /** + * Helper struct combining the project's directory and its type. + */ + struct GameEntry { + FilesystemView fs; + ProjectType type; + }; + /** * Quits FileFinder. */ @@ -287,6 +311,12 @@ namespace FileFinder { */ bool IsRPG2kProjectWithRenames(const FilesystemView& fs); + /** + * @param p fs Tree to check + * @return Project type whether the tree contains a supported project type, known but unsupported engines, or something unknown + */ + ProjectType GetProjectType(const FilesystemView& fs); + /** * Determines if the directory contains a single file/directory ending in ".easyrpg" for use in the * autostart feature. From 8c636c8da09de5d9692915ce3971dfa2a7be3352 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 00:12:59 +0100 Subject: [PATCH 02/18] Change FileFinder::FindGames to find and return game entries (including unsupported ones) --- .../org_easyrpg_player_game_browser.cpp | 16 +++++++++++----- src/filefinder.cpp | 13 +++++++------ src/filefinder.h | 4 ++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index dc3ded6f8a..146501f97c 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -189,7 +189,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, auto root = FileFinder::Root().Create(spath); root.ClearCache(); - std::vector fs_list = FileFinder::FindGames(root); + 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); @@ -204,13 +204,19 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, std::string root_path = FileFinder::GetFullFilesystemPath(root); bool game_in_main_dir = false; if (fs_list.size() == 1) { - if (root_path == FileFinder::GetFullFilesystemPath(fs_list[0])) { - game_in_main_dir = true; - } + if (fs_list[0].type == FileFinder::ProjectType::Supported && + root_path == FileFinder::GetFullFilesystemPath(fs_list[0].fs)) { + game_in_main_dir = true; + } } for (size_t i = 0; i < fs_list.size(); ++i) { - auto& fs = fs_list[i]; + auto& ge = fs_list[i]; + if (ge.type > FileFinder::ProjectType::Supported) { + continue; + } + + auto& fs = ge.fs; std::string full_path = FileFinder::GetFullFilesystemPath(fs); std::string game_dir_name; diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 74d427c397..8546e632ae 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -562,18 +562,19 @@ void FileFinder::DumpFilesystem(FilesystemView fs) { } } -std::vector FileFinder::FindGames(FilesystemView fs, int recursion_limit, int game_limit) { - std::vector games; +std::vector FileFinder::FindGames(FilesystemView fs, int recursion_limit, int game_limit) { + std::vector games; std::function find_recursive = [&](FilesystemView subfs, int rec_limit) -> void { if (!subfs || rec_limit == 0 || static_cast(games.size()) >= game_limit) { return; } - if (IsValidProject(subfs)) { - games.push_back(subfs); - return; - } + auto project_type = GetProjectType(subfs); + if (project_type != ProjectType::Unknown) { + games.push_back({ subfs, project_type }); + return; + } auto entries = subfs.ListDirectory(); diff --git a/src/filefinder.h b/src/filefinder.h index 3bf844e624..1b40b4c2d2 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -390,9 +390,9 @@ namespace FileFinder { * @param fs Filesystem where the search starts * @param recursion_limit Recursion depth * @param game_limit Abort the search when this amount of games was found. - * @return Vector of views to the found game directories + * @return Vector of game entries (filesystem view + project type) found */ - std::vector FindGames(FilesystemView fs, int recursion_limit = 3, int game_limit = 5); + std::vector FindGames(FilesystemView fs, int recursion_limit = 3, int game_limit = 5); } // namespace FileFinder template From e21e7c10884aaffbdef0885ce16df6d31536d0a5 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 02:30:21 +0100 Subject: [PATCH 03/18] Add creating a Game instance from unsupported project type --- .../org_easyrpg_player_game_browser.cpp | 19 +++++++++--- .../org/easyrpg/player/game_browser/Game.java | 24 ++++++++++++--- .../player/game_browser/GameScanner.java | 2 +- .../player/game_browser/ProjectType.java | 29 +++++++++++++++++++ 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index 146501f97c..03e6e3b8f6 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -199,7 +199,8 @@ 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_unsupported = env->GetMethodID(jgame_class, "", "(I)V"); + jmethodID jgame_constructor_supported = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;[BI)V"); std::string root_path = FileFinder::GetFullFilesystemPath(root); bool game_in_main_dir = false; @@ -212,12 +213,22 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, for (size_t i = 0; i < fs_list.size(); ++i) { auto& ge = fs_list[i]; + auto& fs = ge.fs; + + // If game is unsupported, create a Game object with only directory name as title and project type id and continue early if (ge.type > FileFinder::ProjectType::Supported) { + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor_unsupported, (int)ge.type); + + // Use the directory name as the title + auto dir = jstring_to_string(env, jmain_dir_name); + jstring jtitle = env->NewStringUTF(dir.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); continue; } - auto& fs = ge.fs; - std::string full_path = FileFinder::GetFullFilesystemPath(fs); std::string game_dir_name; if (game_in_main_dir) { @@ -354,7 +365,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, /* Create an instance of "Game" */ 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); + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor_supported, jgame_path, jsave_path, title_image, (int)ge.type); if (title_from_ini) { // Store the raw string in the Game instance so it can be reencoded later via user setting 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 ce3df09710..468baadea8 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 @@ -35,9 +35,18 @@ public class Game implements Comparable { private Bitmap titleScreen = null; /** Game is launched from the APK via standalone mode */ private boolean standalone = false; + /** Associated project type. Used to differentiane between supported engines and known but unsupported engines */ + private ProjectType projectType = ProjectType.UNKNOWN; - public Game(String gameFolderPath, String saveFolder, byte[] titleScreen) { + public Game(int projectTypeId) { + this.gameFolderPath = ""; + this.gameFolderName = ""; + this.projectType = ProjectType.getProjectType(projectTypeId); + } + + public Game(String gameFolderPath, String saveFolder, byte[] titleScreen, int projectTypeId) { this.gameFolderPath = gameFolderPath; + this.projectType = ProjectType.getProjectType(projectTypeId); // is only relative here, launchGame will put this in the "saves" directory if (!saveFolder.isEmpty()) { @@ -176,10 +185,15 @@ public String toString() { public static Game fromCacheEntry(Context context, String cache) { String[] entries = cache.split(String.valueOf(escapeCode)); - if (entries.length != 7 || !entries[0].equals(cacheVersion)) { + if (entries.length != 8 || !entries[0].equals(cacheVersion)) { return null; } + int parsedProjectType = Integer.parseInt(entries[7]); + if (parsedProjectType > ProjectType.SUPPORTED.ordinal()) { + return new Game(parsedProjectType); + } + String savePath = entries[1]; DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[2])); if (gameFolder == null) { @@ -200,7 +214,7 @@ public static Game fromCacheEntry(Context context, String cache) { titleScreen = Base64.decode(entries[6], 0); } - Game g = new Game(entries[2], savePath, titleScreen); + Game g = new Game(entries[2], savePath, titleScreen, parsedProjectType); g.setTitle(title); g.titleRaw = titleRaw; @@ -216,7 +230,7 @@ public static Game fromCacheEntry(Context context, String cache) { public String toCacheEntry() { StringBuilder sb = new StringBuilder(); - // Cache structure: savePath | gameFolderPath | title | titleRaw | titleScreen + // Cache structure: savePath | gameFolderPath | title | titleRaw | titleScreen | projectType sb.append(cacheVersion); // 0 sb.append(escapeCode); sb.append(savePath); // 1 @@ -241,6 +255,8 @@ public String toCacheEntry() { } else { sb.append("null"); } + sb.append(escapeCode); + sb.append(projectType.ordinal()); return sb.toString(); } 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 7fddba2362..543c77f0df 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,7 +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 + sb.append("3"); // 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/game_browser/ProjectType.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java new file mode 100644 index 0000000000..f45f418768 --- /dev/null +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java @@ -0,0 +1,29 @@ +package org.easyrpg.player.game_browser; + +import android.content.Context; + +import org.easyrpg.player.R; + +public enum ProjectType { + UNKNOWN("Unknown") + , SUPPORTED("Supported") + , RPG_MAKER_XP("RPG Maker XP") + , RPG_MAKER_VX("RPG Maker VX") + , RPG_MAKER_VX_ACE("RPG Maker VX Ace") + , RPG_MAKER_MV_MZ("RPG Maker MV/MZ") + , WOLF_RPG_EDITOR("Wolf RPG Editor"); + + private final String label; + + ProjectType(String label) { + this.label = label; + } + + public String getLabel() { + return this.label; + } + + public static ProjectType getProjectType(int i) { + return ProjectType.values()[i]; + } +} From 9197e55b4d5655adbaab66362941d6244ef1449f Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 03:01:19 +0100 Subject: [PATCH 04/18] Change Game card to include additional information for unsupported project types --- .../java/org/easyrpg/player/InitActivity.java | 3 ++- .../org/easyrpg/player/game_browser/Game.java | 7 +++++++ .../player/game_browser/GameBrowserActivity.java | 14 ++++++++++++++ .../res/layout/browser_game_card_landscape.xml | 15 +++++++++++++++ .../res/layout/browser_game_card_portrait.xml | 16 ++++++++++++++++ .../android/app/src/main/res/values/strings.xml | 1 + 6 files changed, 55 insertions(+), 1 deletion(-) 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 7f939f5504..f022c700c3 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 @@ -15,6 +15,7 @@ import org.easyrpg.player.game_browser.Game; import org.easyrpg.player.game_browser.GameBrowserActivity; import org.easyrpg.player.game_browser.GameBrowserHelper; +import org.easyrpg.player.game_browser.ProjectType; import org.easyrpg.player.player.AssetUtils; import org.easyrpg.player.settings.SettingsManager; @@ -122,7 +123,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, ProjectType.SUPPORTED.ordinal()); 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 468baadea8..92460beb86 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 @@ -261,4 +261,11 @@ public String toCacheEntry() { return sb.toString(); } + public boolean isProjectTypeUnsupported() { + return this.projectType.ordinal() > ProjectType.SUPPORTED.ordinal(); + } + + public String getProjectTypeLabel() { + return this.projectType.getLabel(); + } } 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 41f6c09109..2f73467078 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 @@ -333,6 +333,18 @@ public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public void onBindViewHolder(final ViewHolder holder, final int position) { final Game game = gameList.get(position); + if (game.isProjectTypeUnsupported()) { + // Title + holder.title.setText(game.getDisplayTitle()); + + // Subtitle - engine unsupported message + holder.subtitle.setText(String.format("%s\n%s", activity.getResources().getString(R.string.unsupported_engine), game.getProjectTypeLabel())); + + // Hide settings button + holder.settingsButton.setVisibility(View.INVISIBLE); + return; + } + // Title holder.title.setText(game.getDisplayTitle()); holder.title.setOnClickListener(v -> launchGame(position, false)); @@ -450,12 +462,14 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam public static class ViewHolder extends RecyclerView.ViewHolder { public TextView title; + public TextView subtitle; public ImageView titleScreen; public ImageButton settingsButton, favoriteButton; public ViewHolder(View v) { super(v); this.title = v.findViewById(R.id.title); + this.subtitle = v.findViewById(R.id.subtitle); this.titleScreen = v.findViewById(R.id.screen); this.settingsButton = v.findViewById(R.id.game_browser_thumbnail_option_button); this.favoriteButton = v.findViewById(R.id.game_browser_thumbnail_favorite_button); diff --git a/builds/android/app/src/main/res/layout/browser_game_card_landscape.xml b/builds/android/app/src/main/res/layout/browser_game_card_landscape.xml index d21dd4fef1..5e09b89a17 100644 --- a/builds/android/app/src/main/res/layout/browser_game_card_landscape.xml +++ b/builds/android/app/src/main/res/layout/browser_game_card_landscape.xml @@ -55,6 +55,21 @@ android:gravity="center_horizontal" /> + + + + + Open navigation drawer Close navigation drawer Open Android menu + Unsupported engine: From f0777b041c34d32ea35363be033742a557e755a3 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 03:36:53 +0100 Subject: [PATCH 05/18] Fix crash on loading games from cache --- .../src/main/java/org/easyrpg/player/game_browser/Game.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 92460beb86..3322236b39 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 @@ -191,7 +191,10 @@ public static Game fromCacheEntry(Context context, String cache) { int parsedProjectType = Integer.parseInt(entries[7]); if (parsedProjectType > ProjectType.SUPPORTED.ordinal()) { - return new Game(parsedProjectType); + Game g = new Game(parsedProjectType); + g.setTitle(entries[4]); + + return g; } String savePath = entries[1]; From 92a76cbf2d3e3cc7c8d8b342dbb2598daccd23e1 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 03:37:16 +0100 Subject: [PATCH 06/18] Show unsupported games last on the list --- .../main/java/org/easyrpg/player/game_browser/Game.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 3322236b39..14b21a2107 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 @@ -130,6 +130,14 @@ private boolean isFavoriteFromSettings() { @Override public int compareTo(Game game) { + // Unsupported games last + if (this.projectType == ProjectType.SUPPORTED && game.projectType.ordinal() > ProjectType.SUPPORTED.ordinal()) { + return -1; + } + if (this.projectType.ordinal() > ProjectType.SUPPORTED.ordinal() && game.projectType == ProjectType.SUPPORTED) { + return 1; + } + // Favorites first if (this.isFavorite() && !game.isFavorite()) { return -1; } From 08a4eef464b75ce825dcac197ace41ab83080653 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 04:08:59 +0100 Subject: [PATCH 07/18] Add explanation dialog when trying to launch unsupported game --- .../game_browser/GameBrowserActivity.java | 18 ++++++++++++++++++ .../app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 20 insertions(+) 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 2f73467078..5390e44c71 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 @@ -342,6 +342,12 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { // Hide settings button holder.settingsButton.setVisibility(View.INVISIBLE); + + // Add click listeners + holder.title.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); + holder.subtitle.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); + holder.titleScreen.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); + return; } @@ -460,6 +466,18 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam builder.show(); } + private void showUnsupportedProjectTypeExplaination(final Context context, String projectType) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + + String message = context.getString(R.string.unsupported_engine_explanation) + .replace("$ENGINE", projectType); + builder + .setTitle(R.string.information) + .setMessage(message) + .setNeutralButton(R.string.ok, null); + builder.show(); + } + public static class ViewHolder extends RecyclerView.ViewHolder { public TextView title; public TextView subtitle; diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index 26f314306e..5df7409e02 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -152,4 +152,6 @@ Please tell us in detail what went wrong.\n\n Close navigation drawer Open Android menu Unsupported engine: + The selected game cannot be launched because it uses an unsupported engine: $ENGINE. EasyRPG supports ONLY games made in RPG Maker 2000 and 2003, other engines are out of scope of the project. For games created in newer versions of RPG Maker (XP and up), consider using alternative emulators available on the Play Store. + Information From caaff2498e869ef79aeb8793d6841cbb18f4effe Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 14:59:09 +0100 Subject: [PATCH 08/18] Notify user when trying to launch a game running on a known unsupported engine --- src/filefinder.h | 24 +++++++++++++++++++++++- src/scene_gamebrowser.cpp | 27 +++++++++++++++++++-------- src/window_gamelist.cpp | 5 +++-- src/window_gamelist.h | 4 ++-- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/filefinder.h b/src/filefinder.h index 1b40b4c2d2..25c68c5dfc 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -69,7 +69,10 @@ namespace FileFinder { ProjectType type; }; - /** + /** @return Human readable project type label */ + static const char* GetProjectTypeLabel(ProjectType pt); + + /** * Quits FileFinder. */ void Quit(); @@ -404,4 +407,23 @@ std::string FileFinder::MakePath(lcf::Span components) { return path; } +static inline const char* FileFinder::GetProjectTypeLabel(ProjectType pt) { + switch (pt) { + case Supported: + return "Supported"; + case RpgMakerXp: + return "RPG Maker XP"; + case RpgMakerVx: + return "RPG Maker VX"; + case RpgMakerVxAce: + return "RPG Maker VX Ace"; + case RpgMakerMvMz: + return "RPG Maker MV/MZ"; + case WolfRpgEditor: + return "Wolf RPG Editor"; + default: + return "Unknown"; + } +} + #endif diff --git a/src/scene_gamebrowser.cpp b/src/scene_gamebrowser.cpp index 5a0b700569..3a5fe1c1cf 100644 --- a/src/scene_gamebrowser.cpp +++ b/src/scene_gamebrowser.cpp @@ -188,32 +188,43 @@ void Scene_GameBrowser::BootGame() { return; } - FilesystemView fs; - std::string entry; - std::tie(fs, entry) = gamelist_window->GetGameFilesystem(); + auto ge = gamelist_window->GetGameEntry(); - if (!fs) { + if (!ge.fs) { Output::Warning("The selected file or directory cannot be opened"); load_window->SetVisible(false); game_loading = false; return; } - if (!FileFinder::IsValidProject(fs) && !FileFinder::OpenViewToEasyRpgFile(fs)) { + if (ge.type > FileFinder::ProjectType::Supported) { + // Game is using a known unsupported engine + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); + Output::Warning( + "[{}] Detected an unsupported game engine: {}", + FileFinder::GetPathAndFilename(ge.fs.GetFullPath()).second, + FileFinder::GetProjectTypeLabel(ge.type) + ); + load_window->SetVisible(false); + game_loading = false; + return; + } + + if (ge.type == FileFinder::ProjectType::Unknown && !FileFinder::OpenViewToEasyRpgFile(ge.fs)) { // Not a game: Open as directory load_window->SetVisible(false); game_loading = false; - if (!gamelist_window->Refresh(fs, true)) { + if (!gamelist_window->Refresh(ge.fs, true)) { Output::Warning("The selected file or directory cannot be opened"); return; } - stack.push_back({ fs, gamelist_window->GetIndex() }); + stack.push_back({ ge.fs, gamelist_window->GetIndex() }); gamelist_window->SetIndex(0); return; } - FileFinder::SetGameFilesystem(fs); + FileFinder::SetGameFilesystem(ge.fs); Player::CreateGameObjects(); game_loading = false; diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index bf6b58fed0..43b06ecbad 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -154,6 +154,7 @@ bool Window_GameList::HasValidEntry() { return game_directories.size() > minval; } -std::pair Window_GameList::GetGameFilesystem() const { - return { base_fs.Create(game_directories[GetIndex()]), game_directories[GetIndex()] }; +FileFinder::GameEntry Window_GameList::GetGameEntry() const { + auto fs = base_fs.Create(game_directories[GetIndex()]); + return { fs, FileFinder::GetProjectType(fs) }; } diff --git a/src/window_gamelist.h b/src/window_gamelist.h index a3809c6f86..6f6f74891c 100644 --- a/src/window_gamelist.h +++ b/src/window_gamelist.h @@ -55,9 +55,9 @@ class Window_GameList : public Window_Selectable { bool HasValidEntry(); /** - * @return filesystem and entry name of the selected game + * @return game entry containing filesystem view and project type */ - std::pair GetGameFilesystem() const; + FileFinder::GameEntry GetGameEntry() const; private: FilesystemView base_fs; From 88860813fe9dc5009b973b7cee5cb4627fddb076 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 15:18:30 +0100 Subject: [PATCH 09/18] Show information for unsupported engines directly on the game list --- src/window_gamelist.cpp | 35 +++++++++++++++++++---------------- src/window_gamelist.h | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index 43b06ecbad..7ca9e3562a 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -34,7 +34,7 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) return false; } - game_directories.clear(); + game_entries.clear(); this->show_dotdot = show_dotdot; @@ -55,25 +55,28 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) } if (dir.second.type == DirectoryTree::FileType::Regular) { if (FileFinder::IsSupportedArchiveExtension(dir.second.name)) { - game_directories.emplace_back(dir.second.name); + auto fs = base_fs.Create(dir.second.name); + game_entries.push_back({ fs, FileFinder::GetProjectType(fs) }); } } else if (dir.second.type == DirectoryTree::FileType::Directory) { - game_directories.emplace_back(dir.second.name); + auto fs = base_fs.Create(dir.second.name); + game_entries.push_back({ fs, FileFinder::GetProjectType(fs) }); } } // Sort game list in place - std::sort(game_directories.begin(), game_directories.end(), - [](const std::string& s, const std::string& s2) { - return strcmp(Utils::LowerCase(s).c_str(), Utils::LowerCase(s2).c_str()) <= 0; + std::sort(game_entries.begin(), game_entries.end(), + [](const FileFinder::GameEntry &ge1, const FileFinder::GameEntry &ge2) { + return strcmp(Utils::LowerCase(ge1.fs.GetSubPath()).c_str(), + Utils::LowerCase(ge2.fs.GetSubPath()).c_str()) <= 0; }); if (show_dotdot) { - game_directories.insert(game_directories.begin(), ".."); + game_entries.insert(game_entries.begin(), { base_fs.Create(".."), FileFinder::ProjectType::Unknown }); } if (HasValidEntry()) { - item_max = game_directories.size(); + item_max = game_entries.size(); CreateContents(); @@ -102,13 +105,14 @@ void Window_GameList::DrawItem(int index) { Rect rect = GetItemRect(index); contents->ClearRect(rect); - std::string text; + auto& ge = game_entries[index]; + auto dir_name = FileFinder::GetPathAndFilename(ge.fs.GetSubPath()).second; + contents->TextDraw(rect.x, rect.y, Font::ColorDefault, dir_name); - if (HasValidEntry()) { - text = game_directories[index]; + if (ge.type > FileFinder::ProjectType::Supported) { + auto notice = fmt::format("Unsupported: {}", FileFinder::GetProjectTypeLabel(ge.type)); + contents->TextDraw(rect.width, rect.y, Font::ColorDisabled, notice, Text::AlignRight); } - - contents->TextDraw(rect.x, rect.y, Font::ColorDefault, game_directories[index]); } void Window_GameList::DrawErrorText(bool show_dotdot) { @@ -151,10 +155,9 @@ void Window_GameList::DrawErrorText(bool show_dotdot) { bool Window_GameList::HasValidEntry() { size_t minval = show_dotdot ? 1 : 0; - return game_directories.size() > minval; + return game_entries.size() > minval; } FileFinder::GameEntry Window_GameList::GetGameEntry() const { - auto fs = base_fs.Create(game_directories[GetIndex()]); - return { fs, FileFinder::GetProjectType(fs) }; + return game_entries[GetIndex()]; } diff --git a/src/window_gamelist.h b/src/window_gamelist.h index 6f6f74891c..ac3b13cdbd 100644 --- a/src/window_gamelist.h +++ b/src/window_gamelist.h @@ -61,7 +61,7 @@ class Window_GameList : public Window_Selectable { private: FilesystemView base_fs; - std::vector game_directories; + std::vector game_entries; bool show_dotdot = false; }; From dfc5295e25649fcdf970dac4458d6e620c66d13f Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 15:49:37 +0100 Subject: [PATCH 10/18] Add wildcard file search --- src/directory_tree.cpp | 15 +++++++++++++++ src/directory_tree.h | 22 +++++++++++++++++----- src/filefinder.cpp | 7 +++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/directory_tree.cpp b/src/directory_tree.cpp index 556a08fa4f..1135165a7b 100644 --- a/src/directory_tree.cpp +++ b/src/directory_tree.cpp @@ -51,6 +51,21 @@ std::unique_ptr DirectoryTree::Create(Filesystem& fs) { return tree; } +bool DirectoryTree::WildcardMatch(const StringView& pattern, const StringView& text) { + if (pattern.length() != text.length()) { + return false; + } + + std::string pattern_norm = make_key(pattern); + std::string text_norm = make_key(text); + + return std::equal(pattern_norm.begin(), pattern_norm.end(), + text_norm.begin(), + [](char p, char t) { + return p == '?' || p == t; + }); +} + DirectoryTree::DirectoryListType* DirectoryTree::ListDirectory(StringView path) const { std::vector entries; std::string fs_path = ToString(path); diff --git a/src/directory_tree.h b/src/directory_tree.h index 6232f5814e..7e7e23debf 100644 --- a/src/directory_tree.h +++ b/src/directory_tree.h @@ -145,15 +145,27 @@ class DirectoryTree { /** lowered dir (full path from root) of missing directories */ mutable std::vector dir_missing_cache; + static bool WildcardMatch(const StringView& pattern, const StringView& text); + template auto Find(T& cache, StringView what) const { - auto it = std::lower_bound(cache.begin(), cache.end(), what, [](const auto& e, const auto& w) { - return e.first < w; - }); - if (it != cache.end() && it->first == what) { - return it; + // No wildcard - binary search + if (what.find('?') == StringView::npos) { + auto it = std::lower_bound(cache.begin(), cache.end(), what, [](const auto& e, const auto& w) { + return e.first < w; + }); + if (it != cache.end() && it->first == what) { + return it; + } + return cache.end(); } + // Has wildcard - linear search + for (auto it = cache.begin(); it != cache.end(); ++it) { + if (WildcardMatch(what, it->first)) { + return it; + } + } return cache.end(); } diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 8546e632ae..1701fdca32 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -317,16 +317,15 @@ FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { return FileFinder::ProjectType::Supported; } - // TODO: Find remaining RGSS dlls - if (!fs.FindFile("RGSS104E.dll").empty()) { + if (!fs.FindFile("RGSS10??.dll").empty()) { return FileFinder::ProjectType::RpgMakerXp; } - if (!fs.FindFile("RGSS202E.dll").empty()) { + if (!fs.FindFile("RGSS20??.dll").empty()) { return FileFinder::ProjectType::RpgMakerVx; } - if (!fs.FindFile("System", "RGSS301.dll").empty()) { + if (!fs.FindFile("System", "RGSS30?.dll").empty()) { return FileFinder::ProjectType::RpgMakerVxAce; } From 2d78864d9367e4ce67ea46f2f5673b4ac7ca00f6 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 15:55:41 +0100 Subject: [PATCH 11/18] Split up and change the unsupported engine explanation --- .../easyrpg/player/game_browser/GameBrowserActivity.java | 8 +++++--- builds/android/app/src/main/res/values/strings.xml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) 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 5390e44c71..c6cfc6a640 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 @@ -469,11 +469,13 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam private void showUnsupportedProjectTypeExplaination(final Context context, String projectType) { AlertDialog.Builder builder = new AlertDialog.Builder(context); - String message = context.getString(R.string.unsupported_engine_explanation) - .replace("$ENGINE", projectType); + String part1 = context.getString(R.string.unsupported_engine_explanation_1).replace("$ENGINE", projectType); + String part2 = context.getString(R.string.unsupported_engine_explanation_2); + String part3 = context.getString(R.string.unsupported_engine_explanation_3); + builder .setTitle(R.string.information) - .setMessage(message) + .setMessage(part1 + '\n' + part2 + '\n' + part3) .setNeutralButton(R.string.ok, null); builder.show(); } diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index 5df7409e02..511fff9c79 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -152,6 +152,8 @@ Please tell us in detail what went wrong.\n\n Close navigation drawer Open Android menu Unsupported engine: - The selected game cannot be launched because it uses an unsupported engine: $ENGINE. EasyRPG supports ONLY games made in RPG Maker 2000 and 2003, other engines are out of scope of the project. For games created in newer versions of RPG Maker (XP and up), consider using alternative emulators available on the Play Store. + This game cannot be played in EasyRPG because it was created with $ENGINE. + EasyRPG is designed to support only RPG Maker 2000 and 2003 games, with no plans to expand to other engines. + For software capable of launching this game, please check the Play Store. Information From 35565220b52992f420dd0f6d83920f5ee52d8142 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 16:24:37 +0100 Subject: [PATCH 12/18] Rename fs_list to ge_list for clarity --- .../org_easyrpg_player_game_browser.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index 03e6e3b8f6..34a2d0374c 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -189,12 +189,12 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, auto root = FileFinder::Root().Create(spath); root.ClearCache(); - std::vector fs_list = FileFinder::FindGames(root); + std::vector ge_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); + jobjectArray jgame_array = env->NewObjectArray(ge_list.size(), jgame_class, nullptr); - if (fs_list.empty()) { + if (ge_list.empty()) { // No games found return jgame_array; } @@ -204,15 +204,15 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, std::string root_path = FileFinder::GetFullFilesystemPath(root); bool game_in_main_dir = false; - if (fs_list.size() == 1) { - if (fs_list[0].type == FileFinder::ProjectType::Supported && - root_path == FileFinder::GetFullFilesystemPath(fs_list[0].fs)) { + if (ge_list.size() == 1) { + if (ge_list[0].type == FileFinder::ProjectType::Supported && + root_path == FileFinder::GetFullFilesystemPath(ge_list[0].fs)) { game_in_main_dir = true; } } - for (size_t i = 0; i < fs_list.size(); ++i) { - auto& ge = fs_list[i]; + for (size_t i = 0; i < ge_list.size(); ++i) { + auto& ge = ge_list[i]; auto& fs = ge.fs; // If game is unsupported, create a Game object with only directory name as title and project type id and continue early @@ -253,7 +253,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, } // Append subdirectory when the archive contains more than one game - if (fs_list.size() > 1) { + if (ge_list.size() > 1) { save_path += FileFinder::GetFullFilesystemPath(fs).substr(root_path.size()); } From 945e5227c689c57c91aa6569f26a29ab6faf444c Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 7 Jan 2025 16:31:20 +0100 Subject: [PATCH 13/18] Add more newlines between unsupported project explanation parts, fix typo --- .../player/game_browser/GameBrowserActivity.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 c6cfc6a640..2f2db879ff 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 @@ -7,7 +7,6 @@ import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; -import android.provider.DocumentsContract; import android.text.InputType; import android.util.DisplayMetrics; import android.util.Log; @@ -16,13 +15,11 @@ import android.view.MenuItem; 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; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -344,9 +341,9 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { holder.settingsButton.setVisibility(View.INVISIBLE); // Add click listeners - holder.title.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); - holder.subtitle.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); - holder.titleScreen.setOnClickListener(v -> showUnsupportedProjectTypeExplaination(activity, game.getProjectTypeLabel())); + holder.title.setOnClickListener(v -> showUnsupportedProjectTypeExplanation(activity, game.getProjectTypeLabel())); + holder.subtitle.setOnClickListener(v -> showUnsupportedProjectTypeExplanation(activity, game.getProjectTypeLabel())); + holder.titleScreen.setOnClickListener(v -> showUnsupportedProjectTypeExplanation(activity, game.getProjectTypeLabel())); return; } @@ -466,16 +463,16 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam builder.show(); } - private void showUnsupportedProjectTypeExplaination(final Context context, String projectType) { + private void showUnsupportedProjectTypeExplanation(final Context context, String projectType) { AlertDialog.Builder builder = new AlertDialog.Builder(context); String part1 = context.getString(R.string.unsupported_engine_explanation_1).replace("$ENGINE", projectType); String part2 = context.getString(R.string.unsupported_engine_explanation_2); String part3 = context.getString(R.string.unsupported_engine_explanation_3); - + builder .setTitle(R.string.information) - .setMessage(part1 + '\n' + part2 + '\n' + part3) + .setMessage(part1 + '\n' + '\n' + part2 + '\n' + '\n' + part3) .setNeutralButton(R.string.ok, null); builder.show(); } From cc4975a4f9d606936d88d1aa48fe5d757bcbd8f4 Mon Sep 17 00:00:00 2001 From: Primekick Date: Tue, 14 Jan 2025 12:28:55 +0100 Subject: [PATCH 14/18] Add encrypted 2k3MP to unsupported engines --- .../player/game_browser/ProjectType.java | 3 +- src/filefinder.cpp | 52 ++++---- src/filefinder.h | 121 +++++++++--------- 3 files changed, 92 insertions(+), 84 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java index f45f418768..28b73005f9 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java @@ -11,7 +11,8 @@ public enum ProjectType { , RPG_MAKER_VX("RPG Maker VX") , RPG_MAKER_VX_ACE("RPG Maker VX Ace") , RPG_MAKER_MV_MZ("RPG Maker MV/MZ") - , WOLF_RPG_EDITOR("Wolf RPG Editor"); + , WOLF_RPG_EDITOR("Wolf RPG Editor") + , ENCRYPTED_2K3_MANIACS("Encrypted 2k3 (Maniacs Patch)"); private final String label; diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 1701fdca32..d8f65f499f 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -313,31 +313,35 @@ bool FileFinder::IsRPG2kProjectWithRenames(const FilesystemView& fs) { } FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { - if (IsValidProject(fs)) { - return FileFinder::ProjectType::Supported; - } + if (IsValidProject(fs)) { + return FileFinder::ProjectType::Supported; + } - if (!fs.FindFile("RGSS10??.dll").empty()) { - return FileFinder::ProjectType::RpgMakerXp; - } + if (!fs.FindFile("RGSS10??.dll").empty()) { + return FileFinder::ProjectType::RpgMakerXp; + } - if (!fs.FindFile("RGSS20??.dll").empty()) { - return FileFinder::ProjectType::RpgMakerVx; - } + if (!fs.FindFile("RGSS20??.dll").empty()) { + return FileFinder::ProjectType::RpgMakerVx; + } + + if (!fs.FindFile("System", "RGSS30?.dll").empty()) { + return FileFinder::ProjectType::RpgMakerVxAce; + } - if (!fs.FindFile("System", "RGSS30?.dll").empty()) { - return FileFinder::ProjectType::RpgMakerVxAce; - } + if (!fs.FindFile("nw.dll").empty()) { + return FileFinder::ProjectType::RpgMakerMvMz; + } - if (!fs.FindFile("nw.dll").empty()) { - return FileFinder::ProjectType::RpgMakerMvMz; - } + if (!fs.FindFile("GuruGuruSMF4.dll").empty()) { + return FileFinder::ProjectType::WolfRpgEditor; + } - if (!fs.FindFile("GuruGuruSMF4.dll").empty()) { - return FileFinder::ProjectType::WolfRpgEditor; - } + if (!fs.FindFile("RPG_RT.rs1").empty()) { + return FileFinder::ProjectType::Encrypted2k3Maniacs; + } - return FileFinder::ProjectType::Unknown; + return FileFinder::ProjectType::Unknown; } bool FileFinder::OpenViewToEasyRpgFile(FilesystemView& fs) { @@ -569,11 +573,11 @@ std::vector FileFinder::FindGames(FilesystemView fs, int return; } - auto project_type = GetProjectType(subfs); - if (project_type != ProjectType::Unknown) { - games.push_back({ subfs, project_type }); - return; - } + auto project_type = GetProjectType(subfs); + if (project_type != ProjectType::Unknown) { + games.push_back({ subfs, project_type }); + return; + } auto entries = subfs.ListDirectory(); diff --git a/src/filefinder.h b/src/filefinder.h index 25c68c5dfc..204ba9b3c8 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -45,34 +45,35 @@ namespace FileFinder { constexpr const auto FONTS_TYPES = Utils::MakeSvArray(".fon", ".fnt", ".bdf", ".ttf", ".ttc", ".otf", ".woff2", ".woff"); constexpr const auto TEXT_TYPES = Utils::MakeSvArray(".txt", ".csv", ""); // "" = Complete Filename (incl. extension) provided by the user - /** - * Type of the project. Used to differentiate between supported games (2kX or EasyRPG) - * and known but unsupported (i.e. newer RPG Makers). - */ - enum ProjectType { - Unknown, - // 2kX or EasyRPG - Supported, - // Known unsupported engines - RpgMakerXp, - RpgMakerVx, - RpgMakerVxAce, - RpgMakerMvMz, - WolfRpgEditor, - }; - - /** - * Helper struct combining the project's directory and its type. - */ - struct GameEntry { - FilesystemView fs; - ProjectType type; - }; - - /** @return Human readable project type label */ - static const char* GetProjectTypeLabel(ProjectType pt); - - /** + /** + * Type of the project. Used to differentiate between supported games (2kX or EasyRPG) + * and known but unsupported (i.e. newer RPG Makers). + */ + enum ProjectType { + Unknown, + // 2kX or EasyRPG + Supported, + // Known unsupported engines + RpgMakerXp, + RpgMakerVx, + RpgMakerVxAce, + RpgMakerMvMz, + WolfRpgEditor, + Encrypted2k3Maniacs, + }; + + /** + * Helper struct combining the project's directory and its type. + */ + struct GameEntry { + FilesystemView fs; + ProjectType type; + }; + + /** @return Human readable project type label */ + static const char* GetProjectTypeLabel(ProjectType pt); + + /** * Quits FileFinder. */ void Quit(); @@ -314,11 +315,11 @@ namespace FileFinder { */ bool IsRPG2kProjectWithRenames(const FilesystemView& fs); - /** - * @param p fs Tree to check - * @return Project type whether the tree contains a supported project type, known but unsupported engines, or something unknown - */ - ProjectType GetProjectType(const FilesystemView& fs); + /** + * @param p fs Tree to check + * @return Project type whether the tree contains a supported project type, known but unsupported engines, or something unknown + */ + ProjectType GetProjectType(const FilesystemView& fs); /** * Determines if the directory contains a single file/directory ending in ".easyrpg" for use in the @@ -355,18 +356,18 @@ namespace FileFinder { bool IsMajorUpdatedTree(); /** RPG_RT.exe file size thresholds - * - * 2k v1.51 (Japanese) : 746496 - * 2k v1.50 (Japanese) : 745984 - * -- threshold (2k) -- : 735000 - * 2k v1.10 (Japanese) : 726016 - * - * 2k3 v1.09a (Japanese) : 950784 - * 2k3 v1.06 (Japanese) : 949248 - * 2k3 v1.05 (Japanese) : unknown - * -- threshold (2k3) -- : 927000 - * 2k3 v1.04 (Japanese) : 913408 - */ + * + * 2k v1.51 (Japanese) : 746496 + * 2k v1.50 (Japanese) : 745984 + * -- threshold (2k) -- : 735000 + * 2k v1.10 (Japanese) : 726016 + * + * 2k3 v1.09a (Japanese) : 950784 + * 2k3 v1.06 (Japanese) : 949248 + * 2k3 v1.05 (Japanese) : unknown + * -- threshold (2k3) -- : 927000 + * 2k3 v1.04 (Japanese) : 913408 + */ enum RpgrtMajorUpdateThreshold { RPG2K = 735000, RPG2K3 = 927000, @@ -408,22 +409,24 @@ std::string FileFinder::MakePath(lcf::Span components) { } static inline const char* FileFinder::GetProjectTypeLabel(ProjectType pt) { - switch (pt) { - case Supported: - return "Supported"; - case RpgMakerXp: - return "RPG Maker XP"; - case RpgMakerVx: - return "RPG Maker VX"; - case RpgMakerVxAce: - return "RPG Maker VX Ace"; - case RpgMakerMvMz: - return "RPG Maker MV/MZ"; - case WolfRpgEditor: - return "Wolf RPG Editor"; + switch (pt) { + case Supported: + return "Supported"; + case RpgMakerXp: + return "RPG Maker XP"; + case RpgMakerVx: + return "RPG Maker VX"; + case RpgMakerVxAce: + return "RPG Maker VX Ace"; + case RpgMakerMvMz: + return "RPG Maker MV/MZ"; + case WolfRpgEditor: + return "Wolf RPG Editor"; + case Encrypted2k3Maniacs: + return "Encrypted 2k3MP"; default: return "Unknown"; - } + } } #endif From 305a1ead41e7d8ab3afc256caeda756275197729 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 16:36:16 +0100 Subject: [PATCH 15/18] Android: Fix "Show folder name" setting for unsupported games Make localization simpler by merging some strings --- .../org_easyrpg_player_game_browser.cpp | 36 ++++++------- .../org/easyrpg/player/game_browser/Game.java | 54 +++++++++---------- .../game_browser/GameBrowserActivity.java | 10 ++-- .../app/src/main/res/values/strings.xml | 8 ++- 4 files changed, 49 insertions(+), 59 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index 34a2d0374c..3010d22e0c 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -205,29 +205,14 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, std::string root_path = FileFinder::GetFullFilesystemPath(root); bool game_in_main_dir = false; if (ge_list.size() == 1) { - if (ge_list[0].type == FileFinder::ProjectType::Supported && - root_path == FileFinder::GetFullFilesystemPath(ge_list[0].fs)) { - game_in_main_dir = true; - } + if (root_path == FileFinder::GetFullFilesystemPath(ge_list[0].fs)) { + game_in_main_dir = true; + } } for (size_t i = 0; i < ge_list.size(); ++i) { auto& ge = ge_list[i]; - auto& fs = ge.fs; - - // If game is unsupported, create a Game object with only directory name as title and project type id and continue early - if (ge.type > FileFinder::ProjectType::Supported) { - jobject jgame_object = env->NewObject(jgame_class, jgame_constructor_unsupported, (int)ge.type); - - // Use the directory name as the title - auto dir = jstring_to_string(env, jmain_dir_name); - jstring jtitle = env->NewStringUTF(dir.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); - continue; - } + auto& fs = ge.fs; std::string full_path = FileFinder::GetFullFilesystemPath(fs); std::string game_dir_name; @@ -239,6 +224,19 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, game_dir_name = std::get<1>(FileFinder::GetPathAndFilename(fs.GetFullPath())); } + // If game is unsupported, create a Game object with only directory name as title and project type id and continue early + if (ge.type > FileFinder::ProjectType::Supported) { + jobject jgame_object = env->NewObject(jgame_class, jgame_constructor_unsupported, (int)ge.type); + + // Use the directory name as the title + jstring jfolder = env->NewStringUTF(game_dir_name.c_str()); + jmethodID jset_folder_name_method = env->GetMethodID(jgame_class, "setGameFolderName", "(Ljava/lang/String;)V"); + env->CallVoidMethod(jgame_object, jset_folder_name_method, jfolder); + + env->SetObjectArrayElement(jgame_array, i, jgame_object); + continue; + } + std::string save_path; if (!fs.IsFeatureSupported(Filesystem::Feature::Write)) { // Is an archive and needs a redirected save path 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 14b21a2107..d9c67217c8 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,15 +16,14 @@ public class Game implements Comparable { final static char escapeCode = '\u0001'; - final static String cacheVersion = "1"; /** The title shown in the Game Browser */ - private String title; + private String title = ""; /** Bytes of the title string in an unspecified encoding */ private byte[] titleRaw = null; /** Human readable version of the game directory. Shown in the game browser * when the specific setting is enabled. */ - private String gameFolderName; + private String gameFolderName = ""; /** 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) */ @@ -39,9 +38,8 @@ public class Game implements Comparable { private ProjectType projectType = ProjectType.UNKNOWN; public Game(int projectTypeId) { - this.gameFolderPath = ""; - this.gameFolderName = ""; this.projectType = ProjectType.getProjectType(projectTypeId); + this.gameFolderPath = ""; } public Game(String gameFolderPath, String saveFolder, byte[] titleScreen, int projectTypeId) { @@ -66,7 +64,7 @@ public String getDisplayTitle() { return customTitle; } - if (SettingsManager.getGameBrowserLabelMode() == 0) { + if (SettingsManager.getGameBrowserLabelMode() == 0 && !getTitle().isEmpty()) { return getTitle(); } else { return gameFolderName; @@ -193,39 +191,39 @@ public String toString() { public static Game fromCacheEntry(Context context, String cache) { String[] entries = cache.split(String.valueOf(escapeCode)); - if (entries.length != 8 || !entries[0].equals(cacheVersion)) { + if (entries.length != 7) { return null; } - int parsedProjectType = Integer.parseInt(entries[7]); + int parsedProjectType = Integer.parseInt(entries[6]); if (parsedProjectType > ProjectType.SUPPORTED.ordinal()) { + // Unsupported game Game g = new Game(parsedProjectType); - g.setTitle(entries[4]); - + g.setGameFolderName(entries[2]); return g; } - String savePath = entries[1]; - DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[2])); + String savePath = entries[0]; + DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[1])); if (gameFolder == null) { return null; } - String gameFolderName = entries[3]; + String gameFolderName = entries[2]; - String title = entries[4]; + String title = entries[3]; byte[] titleRaw = null; - if (!entries[5].equals("null")) { - titleRaw = Base64.decode(entries[5], 0); + if (!entries[4].equals("null")) { + titleRaw = Base64.decode(entries[4], 0); } byte[] titleScreen = null; - if (!entries[6].equals("null")) { - titleScreen = Base64.decode(entries[6], 0); + if (!entries[5].equals("null")) { + titleScreen = Base64.decode(entries[5], 0); } - Game g = new Game(entries[2], savePath, titleScreen, parsedProjectType); + Game g = new Game(entries[1], savePath, titleScreen, parsedProjectType); g.setTitle(title); g.titleRaw = titleRaw; @@ -241,24 +239,22 @@ public static Game fromCacheEntry(Context context, String cache) { public String toCacheEntry() { StringBuilder sb = new StringBuilder(); - // Cache structure: savePath | gameFolderPath | title | titleRaw | titleScreen | projectType - sb.append(cacheVersion); // 0 - sb.append(escapeCode); - sb.append(savePath); // 1 + // Cache structure: savePath | gameFolderPath | gameFolderName | title | titleRaw | titleScreen | projectType + sb.append(savePath); // 0 sb.append(escapeCode); - sb.append(gameFolderPath); // 2 + sb.append(gameFolderPath); // 1 sb.append(escapeCode); - sb.append(gameFolderName); // 3 + sb.append(gameFolderName); // 2 sb.append(escapeCode); - sb.append(title); // 4 + sb.append(title); // 3 sb.append(escapeCode); - if (titleRaw != null) { // 5 + if (titleRaw != null) { // 4 sb.append(Base64.encodeToString(titleRaw, Base64.NO_WRAP)); } else { sb.append("null"); } sb.append(escapeCode); - if (titleScreen != null) { // 6 + if (titleScreen != null) { // 5 ByteArrayOutputStream baos = new ByteArrayOutputStream(); titleScreen.compress(Bitmap.CompressFormat.PNG, 90, baos); byte[] b = baos.toByteArray(); @@ -267,7 +263,7 @@ public String toCacheEntry() { sb.append("null"); } sb.append(escapeCode); - sb.append(projectType.ordinal()); + sb.append(projectType.ordinal()); // 6 return sb.toString(); } 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 2f2db879ff..2206c522fb 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 @@ -335,7 +335,7 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { holder.title.setText(game.getDisplayTitle()); // Subtitle - engine unsupported message - holder.subtitle.setText(String.format("%s\n%s", activity.getResources().getString(R.string.unsupported_engine), game.getProjectTypeLabel())); + holder.subtitle.setText(activity.getResources().getString(R.string.unsupported_engine_card).replace("$ENGINE", game.getProjectTypeLabel())); // Hide settings button holder.settingsButton.setVisibility(View.INVISIBLE); @@ -466,13 +466,11 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam private void showUnsupportedProjectTypeExplanation(final Context context, String projectType) { AlertDialog.Builder builder = new AlertDialog.Builder(context); - String part1 = context.getString(R.string.unsupported_engine_explanation_1).replace("$ENGINE", projectType); - String part2 = context.getString(R.string.unsupported_engine_explanation_2); - String part3 = context.getString(R.string.unsupported_engine_explanation_3); + String message = context.getString(R.string.unsupported_engine_explanation).replace("$ENGINE", projectType); builder - .setTitle(R.string.information) - .setMessage(part1 + '\n' + '\n' + part2 + '\n' + '\n' + part3) + .setTitle(R.string.unsupported_engine_title) + .setMessage(message) .setNeutralButton(R.string.ok, null); builder.show(); } diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index 511fff9c79..3ccec56121 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -151,9 +151,7 @@ Please tell us in detail what went wrong.\n\n Open navigation drawer Close navigation drawer Open Android menu - Unsupported engine: - This game cannot be played in EasyRPG because it was created with $ENGINE. - EasyRPG is designed to support only RPG Maker 2000 and 2003 games, with no plans to expand to other engines. - For software capable of launching this game, please check the Play Store. - Information + $ENGINE is unsupported + Unsupported engine + This game cannot be played in EasyRPG Player because it was created with $ENGINE.\n\nEasyRPG Player is designed to support only RPG Maker 2000 and 2003 games, with no plans to expand to other engines.\n\nFor software capable of launching this game, please check the Play Store. From ea40afb30a68b381ed9ab9037a5e07c1d613f697 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 16:48:03 +0100 Subject: [PATCH 16/18] FileFilder: Make wildcard processing an option Add (Sim) RPG Maker 95 to detection --- .../player/game_browser/ProjectType.java | 18 +++--- src/directory_tree.cpp | 6 +- src/directory_tree.h | 23 ++++--- src/filefinder.cpp | 20 +++++- src/filefinder.h | 64 ++++++++----------- src/scene_gamebrowser.cpp | 24 +++---- src/window_gamelist.cpp | 3 +- 7 files changed, 86 insertions(+), 72 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java index 28b73005f9..29f01d93f8 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java @@ -5,14 +5,16 @@ import org.easyrpg.player.R; public enum ProjectType { - UNKNOWN("Unknown") - , SUPPORTED("Supported") - , RPG_MAKER_XP("RPG Maker XP") - , RPG_MAKER_VX("RPG Maker VX") - , RPG_MAKER_VX_ACE("RPG Maker VX Ace") - , RPG_MAKER_MV_MZ("RPG Maker MV/MZ") - , WOLF_RPG_EDITOR("Wolf RPG Editor") - , ENCRYPTED_2K3_MANIACS("Encrypted 2k3 (Maniacs Patch)"); + UNKNOWN("Unknown"), + SUPPORTED("Supported"), + RPG_MAKER_XP("RPG Maker XP"), + RPG_MAKER_VX("RPG Maker VX"), + RPG_MAKER_VX_ACE("RPG Maker VX Ace"), + RPG_MAKER_MV_MZ("RPG Maker MV/MZ"), + WOLF_RPG_EDITOR("Wolf RPG Editor"), + ENCRYPTED_2K3_MANIACS("Encrypted 2k3 (Maniacs Patch)"), + RPG_MAKER_95("RPG Maker 95"), + SIM_RPG_MAKER_95("Sim RPG Maker 95"); private final String label; diff --git a/src/directory_tree.cpp b/src/directory_tree.cpp index 1135165a7b..d20f4b12b9 100644 --- a/src/directory_tree.cpp +++ b/src/directory_tree.cpp @@ -223,12 +223,12 @@ std::string DirectoryTree::FindFile(const DirectoryTree::Args& args) const { } std::string dir_key = make_key(dir); - auto dir_it = Find(dir_cache, dir_key); + auto dir_it = Find(dir_cache, dir_key, args.process_wildcards); assert(dir_it != dir_cache.end()); std::string name_key = make_key(name); if (args.exts.empty()) { - auto entry_it = Find(*entries, name_key); + auto entry_it = Find(*entries, name_key, args.process_wildcards); if (entry_it != entries->end() && entry_it->second.type == FileType::Regular) { auto full_path = FileFinder::MakePath(dir_it->second, entry_it->second.name); DebugLog("FindFile Found: {} | {} | {}", dir, name, full_path); @@ -237,7 +237,7 @@ std::string DirectoryTree::FindFile(const DirectoryTree::Args& args) const { } else { for (const auto& ext : args.exts) { auto full_name_key = name_key + ToString(ext); - auto entry_it = Find(*entries, full_name_key); + auto entry_it = Find(*entries, full_name_key, args.process_wildcards); if (entry_it != entries->end() && entry_it->second.type == FileType::Regular) { auto full_path = FileFinder::MakePath(dir_it->second, entry_it->second.name); DebugLog("FindFile Found: {} | {} | {}", dir, name, full_path); diff --git a/src/directory_tree.h b/src/directory_tree.h index 7e7e23debf..b5d7959846 100644 --- a/src/directory_tree.h +++ b/src/directory_tree.h @@ -73,6 +73,10 @@ class DirectoryTree { * Off by default because file probing would spam the terminal alot. */ bool file_not_found_warning = false; + /** + * Processes ? in filenames as placeholders + */ + bool process_wildcards = false; }; using DirectoryListType = std::vector>; @@ -148,9 +152,9 @@ class DirectoryTree { static bool WildcardMatch(const StringView& pattern, const StringView& text); template - auto Find(T& cache, StringView what) const { - // No wildcard - binary search - if (what.find('?') == StringView::npos) { + auto Find(T& cache, StringView what, bool process_wildcards = false) const { + if (!process_wildcards) { + // No wildcard - binary search auto it = std::lower_bound(cache.begin(), cache.end(), what, [](const auto& e, const auto& w) { return e.first < w; }); @@ -158,14 +162,15 @@ class DirectoryTree { return it; } return cache.end(); - } - - // Has wildcard - linear search - for (auto it = cache.begin(); it != cache.end(); ++it) { - if (WildcardMatch(what, it->first)) { - return it; + } else { + // Has wildcard - linear search + for (auto it = cache.begin(); it != cache.end(); ++it) { + if (WildcardMatch(what, it->first)) { + return it; + } } } + return cache.end(); } diff --git a/src/filefinder.cpp b/src/filefinder.cpp index d8f65f499f..55b8c920ea 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -317,15 +317,21 @@ FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { return FileFinder::ProjectType::Supported; } - if (!fs.FindFile("RGSS10??.dll").empty()) { + DirectoryTree::Args args; + args.process_wildcards = true; + args.path = "RGSS10??.dll"; + + if (!fs.FindFile(args).empty()) { return FileFinder::ProjectType::RpgMakerXp; } - if (!fs.FindFile("RGSS20??.dll").empty()) { + args.path = "RGSS20??.dll"; + if (!fs.FindFile(args).empty()) { return FileFinder::ProjectType::RpgMakerVx; } - if (!fs.FindFile("System", "RGSS30?.dll").empty()) { + args.path = "System/RGSS30?.dll"; + if (!fs.FindFile(args).empty()) { return FileFinder::ProjectType::RpgMakerVxAce; } @@ -341,6 +347,14 @@ FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { return FileFinder::ProjectType::Encrypted2k3Maniacs; } + if (!fs.FindFile("Game.RPG").empty()) { + return FileFinder::ProjectType::RpgMaker95; + } + + if (!fs.FindFile("Game.DAT").empty()) { + return FileFinder::ProjectType::SimRpgMaker95; + } + return FileFinder::ProjectType::Unknown; } diff --git a/src/filefinder.h b/src/filefinder.h index 204ba9b3c8..7e5e3a0651 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -25,6 +25,7 @@ #include "string_view.h" #include "directory_tree.h" +#include #include #include #include @@ -60,8 +61,23 @@ namespace FileFinder { RpgMakerMvMz, WolfRpgEditor, Encrypted2k3Maniacs, + RpgMaker95, + SimRpgMaker95 }; + constexpr auto kProjectType = lcf::makeEnumTags( + "Unknown", + "Supported", + "RPG Maker XP", + "RPG Maker VX", + "RPG Maker VX Ace", + "RPG Maker MV/MZ", + "Wolf RPG Editor", + "Encrypted 2k3MP", + "RPG Maker 95", + "Sim RPG Maker 95" + ); + /** * Helper struct combining the project's directory and its type. */ @@ -70,9 +86,6 @@ namespace FileFinder { ProjectType type; }; - /** @return Human readable project type label */ - static const char* GetProjectTypeLabel(ProjectType pt); - /** * Quits FileFinder. */ @@ -356,18 +369,18 @@ namespace FileFinder { bool IsMajorUpdatedTree(); /** RPG_RT.exe file size thresholds - * - * 2k v1.51 (Japanese) : 746496 - * 2k v1.50 (Japanese) : 745984 - * -- threshold (2k) -- : 735000 - * 2k v1.10 (Japanese) : 726016 - * - * 2k3 v1.09a (Japanese) : 950784 - * 2k3 v1.06 (Japanese) : 949248 - * 2k3 v1.05 (Japanese) : unknown - * -- threshold (2k3) -- : 927000 - * 2k3 v1.04 (Japanese) : 913408 - */ + * + * 2k v1.51 (Japanese) : 746496 + * 2k v1.50 (Japanese) : 745984 + * -- threshold (2k) -- : 735000 + * 2k v1.10 (Japanese) : 726016 + * + * 2k3 v1.09a (Japanese) : 950784 + * 2k3 v1.06 (Japanese) : 949248 + * 2k3 v1.05 (Japanese) : unknown + * -- threshold (2k3) -- : 927000 + * 2k3 v1.04 (Japanese) : 913408 + */ enum RpgrtMajorUpdateThreshold { RPG2K = 735000, RPG2K3 = 927000, @@ -408,25 +421,4 @@ std::string FileFinder::MakePath(lcf::Span components) { return path; } -static inline const char* FileFinder::GetProjectTypeLabel(ProjectType pt) { - switch (pt) { - case Supported: - return "Supported"; - case RpgMakerXp: - return "RPG Maker XP"; - case RpgMakerVx: - return "RPG Maker VX"; - case RpgMakerVxAce: - return "RPG Maker VX Ace"; - case RpgMakerMvMz: - return "RPG Maker MV/MZ"; - case WolfRpgEditor: - return "Wolf RPG Editor"; - case Encrypted2k3Maniacs: - return "Encrypted 2k3MP"; - default: - return "Unknown"; - } -} - #endif diff --git a/src/scene_gamebrowser.cpp b/src/scene_gamebrowser.cpp index 3a5fe1c1cf..913d19c8cf 100644 --- a/src/scene_gamebrowser.cpp +++ b/src/scene_gamebrowser.cpp @@ -197,18 +197,18 @@ void Scene_GameBrowser::BootGame() { return; } - if (ge.type > FileFinder::ProjectType::Supported) { - // Game is using a known unsupported engine - Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); - Output::Warning( - "[{}] Detected an unsupported game engine: {}", - FileFinder::GetPathAndFilename(ge.fs.GetFullPath()).second, - FileFinder::GetProjectTypeLabel(ge.type) - ); - load_window->SetVisible(false); - game_loading = false; - return; - } + if (ge.type > FileFinder::ProjectType::Supported) { + // Game is using a known unsupported engine + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); + Output::Warning( + "[{}] Detected an unsupported game engine: {}", + FileFinder::GetPathAndFilename(ge.fs.GetFullPath()).second, + FileFinder::kProjectType.tag(ge.type) + ); + load_window->SetVisible(false); + game_loading = false; + return; + } if (ge.type == FileFinder::ProjectType::Unknown && !FileFinder::OpenViewToEasyRpgFile(ge.fs)) { // Not a game: Open as directory diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index 7ca9e3562a..7d7cddf743 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -19,6 +19,7 @@ #include #include #include "window_gamelist.h" +#include "filefinder.h" #include "game_party.h" #include "bitmap.h" #include "font.h" @@ -110,7 +111,7 @@ void Window_GameList::DrawItem(int index) { contents->TextDraw(rect.x, rect.y, Font::ColorDefault, dir_name); if (ge.type > FileFinder::ProjectType::Supported) { - auto notice = fmt::format("Unsupported: {}", FileFinder::GetProjectTypeLabel(ge.type)); + auto notice = fmt::format("Unsupported: {}", FileFinder::kProjectType.tag(ge.type)); contents->TextDraw(rect.width, rect.y, Font::ColorDisabled, notice, Text::AlignRight); } } From 9dc544ecb1cdcc0d6af66e993c89aa8af85bf776 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 17:49:25 +0100 Subject: [PATCH 17/18] Game Browser: Only determine the project type on Windows/UNIX platforms Our homebrew platforms have too slow IO for this. --- .../org_easyrpg_player_game_browser.cpp | 2 +- src/filefinder.cpp | 4 +- src/filefinder.h | 12 ++++- src/scene_gamebrowser.cpp | 25 ++++++---- src/window_gamelist.cpp | 48 +++++++++++++------ src/window_gamelist.h | 5 +- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp index 3010d22e0c..a9d75ed0e4 100644 --- a/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp +++ b/builds/android/app/src/gamebrowser/org_easyrpg_player_game_browser.cpp @@ -189,7 +189,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, auto root = FileFinder::Root().Create(spath); root.ClearCache(); - std::vector ge_list = FileFinder::FindGames(root); + auto ge_list = FileFinder::FindGames(root); jclass jgame_class = env->FindClass("org/easyrpg/player/game_browser/Game"); jobjectArray jgame_array = env->NewObjectArray(ge_list.size(), jgame_class, nullptr); diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 55b8c920ea..dfb680e7f7 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -579,8 +579,8 @@ void FileFinder::DumpFilesystem(FilesystemView fs) { } } -std::vector FileFinder::FindGames(FilesystemView fs, int recursion_limit, int game_limit) { - std::vector games; +std::vector FileFinder::FindGames(FilesystemView fs, int recursion_limit, int game_limit) { + std::vector games; std::function find_recursive = [&](FilesystemView subfs, int rec_limit) -> void { if (!subfs || rec_limit == 0 || static_cast(games.size()) >= game_limit) { diff --git a/src/filefinder.h b/src/filefinder.h index 7e5e3a0651..ec24d0f6a6 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -79,9 +79,17 @@ namespace FileFinder { ); /** - * Helper struct combining the project's directory and its type. + * Helper struct combining the project's directory and its type (used by Game Browser) */ struct GameEntry { + std::string dir_name; + ProjectType type; + }; + + /** + * Helper struct combining project type and filesystem (used by Android Game Browser) + */ + struct FsEntry { FilesystemView fs; ProjectType type; }; @@ -409,7 +417,7 @@ namespace FileFinder { * @param game_limit Abort the search when this amount of games was found. * @return Vector of game entries (filesystem view + project type) found */ - std::vector FindGames(FilesystemView fs, int recursion_limit = 3, int game_limit = 5); + std::vector FindGames(FilesystemView fs, int recursion_limit = 3, int game_limit = 5); } // namespace FileFinder template diff --git a/src/scene_gamebrowser.cpp b/src/scene_gamebrowser.cpp index 913d19c8cf..1e3447d5b4 100644 --- a/src/scene_gamebrowser.cpp +++ b/src/scene_gamebrowser.cpp @@ -188,43 +188,48 @@ void Scene_GameBrowser::BootGame() { return; } - auto ge = gamelist_window->GetGameEntry(); + auto entry = gamelist_window->GetFilesystemEntry(); - if (!ge.fs) { + if (!entry.fs) { Output::Warning("The selected file or directory cannot be opened"); load_window->SetVisible(false); game_loading = false; return; } - if (ge.type > FileFinder::ProjectType::Supported) { + if (entry.type == FileFinder::ProjectType::Unknown) { + // Fetched again for platforms where the type is not populated due to bad IO performance + entry.type = FileFinder::GetProjectType(entry.fs); + } + + if (entry.type > FileFinder::ProjectType::Supported) { // Game is using a known unsupported engine Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); Output::Warning( - "[{}] Detected an unsupported game engine: {}", - FileFinder::GetPathAndFilename(ge.fs.GetFullPath()).second, - FileFinder::kProjectType.tag(ge.type) + "{} has unsupported engine {}", + FileFinder::GetPathAndFilename(entry.fs.GetFullPath()).second, + FileFinder::kProjectType.tag(entry.type) ); load_window->SetVisible(false); game_loading = false; return; } - if (ge.type == FileFinder::ProjectType::Unknown && !FileFinder::OpenViewToEasyRpgFile(ge.fs)) { + if (entry.type == FileFinder::ProjectType::Unknown && !FileFinder::OpenViewToEasyRpgFile(entry.fs)) { // Not a game: Open as directory load_window->SetVisible(false); game_loading = false; - if (!gamelist_window->Refresh(ge.fs, true)) { + if (!gamelist_window->Refresh(entry.fs, true)) { Output::Warning("The selected file or directory cannot be opened"); return; } - stack.push_back({ ge.fs, gamelist_window->GetIndex() }); + stack.push_back({ entry.fs, gamelist_window->GetIndex() }); gamelist_window->SetIndex(0); return; } - FileFinder::SetGameFilesystem(ge.fs); + FileFinder::SetGameFilesystem(entry.fs); Player::CreateGameObjects(); game_loading = false; diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index 7d7cddf743..6fd0bb17d5 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -16,13 +16,11 @@ */ // Headers -#include -#include #include "window_gamelist.h" #include "filefinder.h" -#include "game_party.h" #include "bitmap.h" #include "font.h" +#include "system.h" Window_GameList::Window_GameList(int ix, int iy, int iwidth, int iheight) : Window_Selectable(ix, iy, iwidth, iheight) { @@ -56,24 +54,34 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) } if (dir.second.type == DirectoryTree::FileType::Regular) { if (FileFinder::IsSupportedArchiveExtension(dir.second.name)) { + // The type is only determined on platforms with fast file IO (Windows and UNIX systems) + // A platform is considered "fast" when it does not require our custom IO buffer +#ifndef USE_CUSTOM_FILEBUF auto fs = base_fs.Create(dir.second.name); - game_entries.push_back({ fs, FileFinder::GetProjectType(fs) }); + game_entries.push_back({ dir.second.name, FileFinder::GetProjectType(fs) }); +#else + game_entries.push_back({ dir.second.name, FileFinder::ProjectType::Unknown }); +#endif } } else if (dir.second.type == DirectoryTree::FileType::Directory) { +#ifndef USE_CUSTOM_FILEBUF auto fs = base_fs.Create(dir.second.name); - game_entries.push_back({ fs, FileFinder::GetProjectType(fs) }); + game_entries.push_back({ dir.second.name, FileFinder::GetProjectType(fs) }); +#else + game_entries.push_back({ dir.second.name, FileFinder::ProjectType::Unknown }); +#endif } } // Sort game list in place std::sort(game_entries.begin(), game_entries.end(), [](const FileFinder::GameEntry &ge1, const FileFinder::GameEntry &ge2) { - return strcmp(Utils::LowerCase(ge1.fs.GetSubPath()).c_str(), - Utils::LowerCase(ge2.fs.GetSubPath()).c_str()) <= 0; + return strcmp(Utils::LowerCase(ge1.dir_name).c_str(), + Utils::LowerCase(ge2.dir_name).c_str()) <= 0; }); if (show_dotdot) { - game_entries.insert(game_entries.begin(), { base_fs.Create(".."), FileFinder::ProjectType::Unknown }); + game_entries.insert(game_entries.begin(), { "..", FileFinder::ProjectType::Unknown }); } if (HasValidEntry()) { @@ -107,12 +115,23 @@ void Window_GameList::DrawItem(int index) { contents->ClearRect(rect); auto& ge = game_entries[index]; - auto dir_name = FileFinder::GetPathAndFilename(ge.fs.GetSubPath()).second; - contents->TextDraw(rect.x, rect.y, Font::ColorDefault, dir_name); + +#ifndef USE_CUSTOM_FILEBUF + auto color = Font::ColorDefault; + if (ge.type == FileFinder::Unknown) { + color = Font::ColorHeal; + } else if (ge.type > FileFinder::ProjectType::Supported) { + color = Font::ColorKnockout; + } +#else + auto color = Font::ColorDefault; +#endif + + contents->TextDraw(rect.x, rect.y, color, ge.dir_name); if (ge.type > FileFinder::ProjectType::Supported) { - auto notice = fmt::format("Unsupported: {}", FileFinder::kProjectType.tag(ge.type)); - contents->TextDraw(rect.width, rect.y, Font::ColorDisabled, notice, Text::AlignRight); + auto notice = fmt::format("{}", FileFinder::kProjectType.tag(ge.type)); + contents->TextDraw(rect.width, rect.y, color, notice, Text::AlignRight); } } @@ -159,6 +178,7 @@ bool Window_GameList::HasValidEntry() { return game_entries.size() > minval; } -FileFinder::GameEntry Window_GameList::GetGameEntry() const { - return game_entries[GetIndex()]; +FileFinder::FsEntry Window_GameList::GetFilesystemEntry() const { + const auto& entry = game_entries[GetIndex()]; + return { base_fs.Create(entry.dir_name), entry.type }; } diff --git a/src/window_gamelist.h b/src/window_gamelist.h index ac3b13cdbd..bf0a39f737 100644 --- a/src/window_gamelist.h +++ b/src/window_gamelist.h @@ -20,7 +20,6 @@ // Headers #include -#include "window_help.h" #include "window_selectable.h" #include "filefinder.h" @@ -55,9 +54,9 @@ class Window_GameList : public Window_Selectable { bool HasValidEntry(); /** - * @return game entry containing filesystem view and project type + * @return fs entry containing filesystem view and project type */ - FileFinder::GameEntry GetGameEntry() const; + FileFinder::FsEntry GetFilesystemEntry() const; private: FilesystemView base_fs; From 2c19a177dfdee519f2fed7729d052ce004ee99f7 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 19:03:54 +0100 Subject: [PATCH 18/18] (Sim)RM95: Use detection suggested by @florianessl --- src/directory_tree.cpp | 37 ++++++++++++++++++++++++++++--------- src/filefinder.cpp | 12 ++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/directory_tree.cpp b/src/directory_tree.cpp index d20f4b12b9..3397f96fff 100644 --- a/src/directory_tree.cpp +++ b/src/directory_tree.cpp @@ -52,18 +52,37 @@ std::unique_ptr DirectoryTree::Create(Filesystem& fs) { } bool DirectoryTree::WildcardMatch(const StringView& pattern, const StringView& text) { - if (pattern.length() != text.length()) { - return false; + // Limitations: * and ? cannot be mixed, * only at beginning and end of string + // Pattern and text are already normalized + if (pattern.empty() && text.empty()) { + return true; } - std::string pattern_norm = make_key(pattern); - std::string text_norm = make_key(text); + bool begin_wildcard = pattern.starts_with('*'); + bool end_wildcard = pattern.ends_with('*'); - return std::equal(pattern_norm.begin(), pattern_norm.end(), - text_norm.begin(), - [](char p, char t) { - return p == '?' || p == t; - }); + if ((begin_wildcard || end_wildcard) && text.size() > 0) { + // * handling + bool found = false; + if (begin_wildcard) { + found |= text.ends_with(pattern.substr(1)); + } + if (end_wildcard) { + found |= text.starts_with(pattern.substr(0, pattern.size() - 1)); + } + return found; + } else { + // ? handling + if (pattern.length() != text.length()) { + return false; + } + + return std::equal(pattern.begin(), pattern.end(), + text.begin(), + [](char p, char t) { + return p == '?' || p == t; + }); + } } DirectoryTree::DirectoryListType* DirectoryTree::ListDirectory(StringView path) const { diff --git a/src/filefinder.cpp b/src/filefinder.cpp index dfb680e7f7..ccc2b841e5 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -347,12 +347,12 @@ FileFinder::ProjectType FileFinder::GetProjectType(const FilesystemView &fs) { return FileFinder::ProjectType::Encrypted2k3Maniacs; } - if (!fs.FindFile("Game.RPG").empty()) { - return FileFinder::ProjectType::RpgMaker95; - } - - if (!fs.FindFile("Game.DAT").empty()) { - return FileFinder::ProjectType::SimRpgMaker95; + if (!fs.FindFile("SWNAME.DAT").empty()) { + if (!fs.FindFile("GEOLOGY.DAT").empty()) { + return FileFinder::ProjectType::SimRpgMaker95; + } else if (args.path = "*.RPG"; !fs.FindFile(args).empty()) { + return FileFinder::ProjectType::RpgMaker95; + } } return FileFinder::ProjectType::Unknown;