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..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,28 +189,30 @@ 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); + auto 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; } - 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; - if (fs_list.size() == 1) { - if (root_path == FileFinder::GetFullFilesystemPath(fs_list[0])) { + if (ge_list.size() == 1) { + if (root_path == FileFinder::GetFullFilesystemPath(ge_list[0].fs)) { game_in_main_dir = true; } } - for (size_t i = 0; i < fs_list.size(); ++i) { - auto& fs = fs_list[i]; + for (size_t i = 0; i < ge_list.size(); ++i) { + auto& ge = ge_list[i]; + auto& fs = ge.fs; std::string full_path = FileFinder::GetFullFilesystemPath(fs); std::string game_dir_name; @@ -222,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 @@ -236,7 +251,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()); } @@ -348,7 +363,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/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 ce3df09710..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) */ @@ -35,9 +34,17 @@ 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.projectType = ProjectType.getProjectType(projectTypeId); + this.gameFolderPath = ""; + } + + 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()) { @@ -57,7 +64,7 @@ public String getDisplayTitle() { return customTitle; } - if (SettingsManager.getGameBrowserLabelMode() == 0) { + if (SettingsManager.getGameBrowserLabelMode() == 0 && !getTitle().isEmpty()) { return getTitle(); } else { return gameFolderName; @@ -121,6 +128,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; } @@ -176,31 +191,39 @@ 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 != 7) { return null; } - String savePath = entries[1]; - DocumentFile gameFolder = DocumentFile.fromTreeUri(context, Uri.parse(entries[2])); + int parsedProjectType = Integer.parseInt(entries[6]); + if (parsedProjectType > ProjectType.SUPPORTED.ordinal()) { + // Unsupported game + Game g = new Game(parsedProjectType); + g.setGameFolderName(entries[2]); + return g; + } + + 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); + Game g = new Game(entries[1], savePath, titleScreen, parsedProjectType); g.setTitle(title); g.titleRaw = titleRaw; @@ -216,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 - sb.append(cacheVersion); // 0 + // Cache structure: savePath | gameFolderPath | gameFolderName | title | titleRaw | titleScreen | projectType + sb.append(savePath); // 0 sb.append(escapeCode); - sb.append(savePath); // 1 + sb.append(gameFolderPath); // 1 sb.append(escapeCode); - sb.append(gameFolderPath); // 2 + sb.append(gameFolderName); // 2 sb.append(escapeCode); - sb.append(gameFolderName); // 3 + sb.append(title); // 3 sb.append(escapeCode); - sb.append(title); // 4 - 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(); @@ -241,8 +262,17 @@ public String toCacheEntry() { } else { sb.append("null"); } + sb.append(escapeCode); + sb.append(projectType.ordinal()); // 6 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..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 @@ -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; @@ -333,6 +330,24 @@ 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(activity.getResources().getString(R.string.unsupported_engine_card).replace("$ENGINE", game.getProjectTypeLabel())); + + // Hide settings button + holder.settingsButton.setVisibility(View.INVISIBLE); + + // Add click listeners + 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; + } + // Title holder.title.setText(game.getDisplayTitle()); holder.title.setOnClickListener(v -> launchGame(position, false)); @@ -448,14 +463,28 @@ public void renameGame(final Context context, final ViewHolder holder, final Gam builder.show(); } + private void showUnsupportedProjectTypeExplanation(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.unsupported_engine_title) + .setMessage(message) + .setNeutralButton(R.string.ok, null); + builder.show(); + } + 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/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..29f01d93f8 --- /dev/null +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/ProjectType.java @@ -0,0 +1,32 @@ +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"), + 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; + + ProjectType(String label) { + this.label = label; + } + + public String getLabel() { + return this.label; + } + + public static ProjectType getProjectType(int i) { + return ProjectType.values()[i]; + } +} 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 + $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. diff --git a/src/directory_tree.cpp b/src/directory_tree.cpp index 556a08fa4f..3397f96fff 100644 --- a/src/directory_tree.cpp +++ b/src/directory_tree.cpp @@ -51,6 +51,40 @@ std::unique_ptr DirectoryTree::Create(Filesystem& fs) { return tree; } +bool DirectoryTree::WildcardMatch(const StringView& pattern, const StringView& text) { + // 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; + } + + bool begin_wildcard = pattern.starts_with('*'); + bool end_wildcard = pattern.ends_with('*'); + + 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 { std::vector entries; std::string fs_path = ToString(path); @@ -208,12 +242,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); @@ -222,7 +256,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 6232f5814e..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>; @@ -145,13 +149,26 @@ 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; + 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; + }); + if (it != cache.end() && it->first == what) { + return it; + } + return cache.end(); + } 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 52e1d4244f..ccc2b841e5 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -312,6 +312,52 @@ 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; + } + + DirectoryTree::Args args; + args.process_wildcards = true; + args.path = "RGSS10??.dll"; + + if (!fs.FindFile(args).empty()) { + return FileFinder::ProjectType::RpgMakerXp; + } + + args.path = "RGSS20??.dll"; + if (!fs.FindFile(args).empty()) { + return FileFinder::ProjectType::RpgMakerVx; + } + + args.path = "System/RGSS30?.dll"; + if (!fs.FindFile(args).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; + } + + if (!fs.FindFile("RPG_RT.rs1").empty()) { + return FileFinder::ProjectType::Encrypted2k3Maniacs; + } + + 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; +} + bool FileFinder::OpenViewToEasyRpgFile(FilesystemView& fs) { auto files = fs.ListDirectory(); if (!files) { @@ -533,16 +579,17 @@ 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); + auto project_type = GetProjectType(subfs); + if (project_type != ProjectType::Unknown) { + games.push_back({ subfs, project_type }); return; } diff --git a/src/filefinder.h b/src/filefinder.h index 0aa5eccdae..ec24d0f6a6 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 @@ -45,6 +46,54 @@ 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, + 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 (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; + }; + /** * Quits FileFinder. */ @@ -287,6 +336,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. @@ -322,18 +377,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, @@ -360,9 +415,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 diff --git a/src/scene_gamebrowser.cpp b/src/scene_gamebrowser.cpp index 5a0b700569..1e3447d5b4 100644 --- a/src/scene_gamebrowser.cpp +++ b/src/scene_gamebrowser.cpp @@ -188,32 +188,48 @@ void Scene_GameBrowser::BootGame() { return; } - FilesystemView fs; - std::string entry; - std::tie(fs, entry) = gamelist_window->GetGameFilesystem(); + auto entry = gamelist_window->GetFilesystemEntry(); - if (!fs) { + if (!entry.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 (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( + "{} has unsupported engine {}", + FileFinder::GetPathAndFilename(entry.fs.GetFullPath()).second, + FileFinder::kProjectType.tag(entry.type) + ); + load_window->SetVisible(false); + game_loading = false; + return; + } + + 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(fs, true)) { + if (!gamelist_window->Refresh(entry.fs, true)) { Output::Warning("The selected file or directory cannot be opened"); return; } - stack.push_back({ fs, gamelist_window->GetIndex() }); + stack.push_back({ entry.fs, gamelist_window->GetIndex() }); gamelist_window->SetIndex(0); return; } - FileFinder::SetGameFilesystem(fs); + FileFinder::SetGameFilesystem(entry.fs); Player::CreateGameObjects(); game_loading = false; diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index bf6b58fed0..6fd0bb17d5 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -16,12 +16,11 @@ */ // Headers -#include -#include #include "window_gamelist.h" -#include "game_party.h" +#include "filefinder.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) { @@ -34,7 +33,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 +54,38 @@ 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); + // 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({ 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) { - game_directories.emplace_back(dir.second.name); +#ifndef USE_CUSTOM_FILEBUF + auto fs = base_fs.Create(dir.second.name); + 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_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.dir_name).c_str(), + Utils::LowerCase(ge2.dir_name).c_str()) <= 0; }); if (show_dotdot) { - game_directories.insert(game_directories.begin(), ".."); + game_entries.insert(game_entries.begin(), { "..", FileFinder::ProjectType::Unknown }); } if (HasValidEntry()) { - item_max = game_directories.size(); + item_max = game_entries.size(); CreateContents(); @@ -102,13 +114,25 @@ void Window_GameList::DrawItem(int index) { Rect rect = GetItemRect(index); contents->ClearRect(rect); - std::string text; + auto& ge = game_entries[index]; - if (HasValidEntry()) { - text = game_directories[index]; +#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, Font::ColorDefault, game_directories[index]); + contents->TextDraw(rect.x, rect.y, color, ge.dir_name); + + if (ge.type > FileFinder::ProjectType::Supported) { + auto notice = fmt::format("{}", FileFinder::kProjectType.tag(ge.type)); + contents->TextDraw(rect.width, rect.y, color, notice, Text::AlignRight); + } } void Window_GameList::DrawErrorText(bool show_dotdot) { @@ -151,9 +175,10 @@ 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; } -std::pair Window_GameList::GetGameFilesystem() const { - return { base_fs.Create(game_directories[GetIndex()]), game_directories[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 a3809c6f86..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,13 +54,13 @@ class Window_GameList : public Window_Selectable { bool HasValidEntry(); /** - * @return filesystem and entry name of the selected game + * @return fs entry containing filesystem view and project type */ - std::pair GetGameFilesystem() const; + FileFinder::FsEntry GetFilesystemEntry() const; private: FilesystemView base_fs; - std::vector game_directories; + std::vector game_entries; bool show_dotdot = false; };