From 576cb9af42420750a83af6d7ee64c01c87f4a332 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sun, 19 Jan 2025 13:27:16 +0100 Subject: [PATCH 1/6] Partial revert of e7767ffb (the code that replaced min/max with clamp) Added comments to warn about not ever doing this again. Fix #3333 --- src/game_map.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/game_map.cpp b/src/game_map.cpp index c4068c5d4..a2621d0ae 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -634,7 +634,9 @@ void Game_Map::Scroll(int dx, int dy) { // that acc changed by. static void ClampingAdd(int low, int high, int& acc, int& inc) { int original_acc = acc; - acc = std::clamp(acc + inc, low, high); + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + acc = std::max(low, std::min(high, acc + inc)); inc = acc - original_acc; } @@ -1648,7 +1650,9 @@ void Game_Map::SetPositionX(int x, bool reset_panorama) { if (LoopHorizontal()) { x = Utils::PositiveModulo(x, map_width); } else { - x = std::clamp(x, 0, map_width - screen_width); + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + x = std::max(0, std::min(map_width - screen_width, x)); } map_info.position_x = x; if (reset_panorama) { @@ -1670,7 +1674,9 @@ void Game_Map::SetPositionY(int y, bool reset_panorama) { if (LoopVertical()) { y = Utils::PositiveModulo(y, map_height); } else { - y = std::clamp(y, 0, map_height - screen_height); + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + y = std::max(0, std::min(map_height - screen_height, y)); } map_info.position_y = y; if (reset_panorama) { From 99137143e43c9ff696d41dc302734b0e3e7af3ca Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 15:39:47 +0100 Subject: [PATCH 2/6] TriggerEventAt: Add flag to configure "Face Player" behaviour The "Triggered By Decision Key" state is now forwarded from the source event. Suggested by @florianessl --- src/game_interpreter_map.cpp | 10 +++++++++- src/game_player.cpp | 12 ++++++------ src/game_player.h | 6 +++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 5a1034e8f..c0445e940 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -871,7 +871,15 @@ bool Game_Interpreter_Map::CommandEasyRpgTriggerEventAt(lcf::rpg::EventCommand c int x = ValueOrVariable(com.parameters[0], com.parameters[1]); int y = ValueOrVariable(com.parameters[2], com.parameters[3]); - Main_Data::game_player->TriggerEventAt(x, y); + // backwards compatible with old (shorter) command + bool face_player = true; + + if (com.parameters.size() > 4) { + int flags = com.parameters[4]; + face_player = (flags & 1) > 0; + } + + Main_Data::game_player->TriggerEventAt(x, y, GetFrame().triggered_by_decision_key, face_player); return true; } diff --git a/src/game_player.cpp b/src/game_player.cpp index 4afc557ea..af0a41ae6 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -428,7 +428,7 @@ bool Game_Player::CheckActionEvent() { return result || got_action; } -bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key) { +bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player) { if (InAirship()) { return false; } @@ -444,13 +444,13 @@ bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_d && trigger >= 0 && triggers[trigger]) { SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, true); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); } } return result; } -bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key) { +bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player) { if (InAirship()) { return false; } @@ -465,7 +465,7 @@ bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool && trigger >= 0 && triggers[trigger]) { SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, true); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); } } return result; @@ -927,6 +927,6 @@ void Game_Player::UpdatePan() { data()->pan_current_y -= dy; } -bool Game_Player::TriggerEventAt(int x, int y) { - return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, true); +bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { + return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); } diff --git a/src/game_player.h b/src/game_player.h index 6710dda2b..9174e1f23 100644 --- a/src/game_player.h +++ b/src/game_player.h @@ -60,7 +60,7 @@ class Game_Player : public Game_PlayerBase { TeleportTarget GetTeleportTarget() const; void ResetTeleportTarget(TeleportTarget tt = {}); - bool TriggerEventAt(int x, int y); + bool TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player); /** * Sets the map, position and direction that the game player must have after the teleport is over @@ -163,8 +163,8 @@ class Game_Player : public Game_PlayerBase { void UpdatePan(); void UpdateEncounterSteps(); bool CheckActionEvent(); - bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key); - bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key); + bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player = true); + bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player = true); bool GetOnVehicle(); bool GetOffVehicle(); bool UpdateAirship(); From a43df20d116b95c0ba61b347339a964c00187812 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 16:18:10 +0100 Subject: [PATCH 3/6] Fix warning (blockType is currently not used) --- src/tilemap_layer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tilemap_layer.cpp b/src/tilemap_layer.cpp index d371fdd18..ae92d87c3 100644 --- a/src/tilemap_layer.cpp +++ b/src/tilemap_layer.cpp @@ -839,7 +839,7 @@ void TilemapLayer::RecalculateAutotile(int x, int y, int tile_id) { return neighbors; }; - auto processBlock = [&](int blockType, int blockStride, int blockBase, auto isSameAutotileFn) { + auto processBlock = [&](int /*blockType*/, int blockStride, int blockBase, auto isSameAutotileFn) { uint8_t neighbors = calculateNeighbors(isSameAutotileFn); int block = (tile_id - blockBase) / blockStride; int variant = AUTOTILE_D_VARIANTS_MAP.at(neighbors); From 7bed7071dfe6e3fb17676845feac7589cf9785ac Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 18:03:30 +0100 Subject: [PATCH 4/6] LZH: Improve error handling, on Android even invalid files want through the entire LZH reader --- .../src/gamebrowser/org_easyrpg_player_game_browser.cpp | 6 +++++- .../org/easyrpg/player/game_browser/GameScanner.java | 4 ++++ src/filesystem_lzh.cpp | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) 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 dc3ded6f8..0a79f8550 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 @@ -187,6 +187,10 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, // jpath is the SAF path to the game, is converted to FilesystemView "root" std::string spath = jstring_to_string(env, jpath); auto root = FileFinder::Root().Create(spath); + if (!root) { + return nullptr; + } + root.ClearCache(); std::vector fs_list = FileFinder::FindGames(root); @@ -196,7 +200,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, if (fs_list.empty()) { // No games found - return jgame_array; + return nullptr; } jmethodID jgame_constructor = env->GetMethodID(jgame_class, "", "(Ljava/lang/String;Ljava/lang/String;[B)V"); 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 7fddba236..349170201 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 @@ -171,6 +171,10 @@ private void scanRootFolder(Activity activity, Uri folderURI) { Game[] candidates = findGames(fileURIs.get(i).toString(), names.get(i)); + if (candidates == null) { + continue; + } + for (Game candidate: candidates) { if (candidate != null) { gameList.add(candidate); diff --git a/src/filesystem_lzh.cpp b/src/filesystem_lzh.cpp index d69de8793..f4925c410 100644 --- a/src/filesystem_lzh.cpp +++ b/src/filesystem_lzh.cpp @@ -99,6 +99,15 @@ LzhFilesystem::LzhFilesystem(std::string base_path, FilesystemView parent_fs, St // Compressed data offset is manually calculated to reduce calls to tellg() auto last_offset = is.tellg(); + // Read one file, when it fails consider it not an Lzh archive + if (lha_reader_next_file(lha_reader.get()) == nullptr) { + Output::Debug("LzhFS: {} is not a valid archive", GetPath()); + return; + } + + is.clear(); + is.seekg(last_offset); + // Guess the encoding if (encoding.empty()) { std::stringstream filename_guess; From 2304561de91e1f13780259b2711f1173ed5be233 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 18:08:39 +0100 Subject: [PATCH 5/6] Android: Open a real file browser on API >= 29 when the open document buttons are used --- .../main/java/org/easyrpg/player/Helper.java | 40 +++++++++++++++++++ .../game_browser/GameBrowserHelper.java | 12 +++++- .../settings/SettingsAudioActivity.java | 16 +------- .../player/settings/SettingsFontActivity.java | 16 +------- .../settings/SettingsGamesFolderActivity.java | 33 ++------------- 5 files changed, 55 insertions(+), 62 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java index dfe119198..4c9d79882 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/Helper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/Helper.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; @@ -17,6 +18,8 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; @@ -356,4 +359,41 @@ public static Bitmap createBitmapFromRGBA(byte[] rgba, int width, int height) { return bitmap; } + public static void attachOpenFolderButton(Context context, Button button, Uri uri) { + if (android.os.Build.VERSION.SDK_INT >= 26) { + button.setOnClickListener(v -> { + openFileBrowser(context, uri); + }); + } else { + // ACTION_OPEN_DOCUMENT does not support providing an URI + // Useless, remove the button + ViewGroup layout = (ViewGroup) button.getParent(); + if(layout != null) { + layout.removeView(button); + } + } + } + + public static boolean openFileBrowser(Context context, Uri uri) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + // Open the file explorer in the folder specified by URI + // This opens a real file browser which allows file operations like view, copy, etc. + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR); + context.startActivity(intent); + } else if (android.os.Build.VERSION.SDK_INT >= 26) { + // Open the file explorer in the folder specified by URI + // This opens a (useless) file chooser which closes itself after selecting a file + // Still better than nothing because the user can see where the folder is + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri); + context.startActivity(intent); + } else { + // ACTION_OPEN_DOCUMENT does not support providing an URI + return false; + } + + return true; + } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java index 131dfd121..fbe4271a0 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserHelper.java @@ -194,14 +194,22 @@ public static SafError dealAfterFolderSelected(Activity activity, int requestCod List items = Helper.listChildrenDocuments(activity, folder.getUri()); int item_count = 0; + for (String[] item: items) { - if (item[0] == null || Helper.isDirectoryFromMimeType(item[1]) || item[0].endsWith(".nomedia")) { + if (item[2] == null || + item[2].contains(".") || + item[2].equals(SettingsManager.RTP_FOLDER_NAME) || + item[2].equals(SettingsManager.GAMES_FOLDER_NAME) || + item[2].equals(SettingsManager.SOUND_FONTS_FOLDER_NAME) || + item[2].equals(SettingsManager.SAVES_FOLDER_NAME) || + item[2].equals(SettingsManager.FONTS_FOLDER_NAME) + ) { continue; } item_count += 1; - if (item_count >= 3) { + if (item_count > 3) { return SafError.FOLDER_NOT_ALMOST_EMPTY; } } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java index f23cc7884..a8fde730b 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsAudioActivity.java @@ -37,21 +37,7 @@ protected void onCreate(Bundle savedInstanceState) { // Setup UI components // The Soundfont Button Button button = this.findViewById(R.id.button_open_soundfont_folder); - // We can open the file picker in a specific folder only with API >= 26 - if (android.os.Build.VERSION.SDK_INT >= 26) { - button.setOnClickListener(v -> { - // Open the file explorer in the "soundfont" folder - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.setType("*/*"); - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SettingsManager.getSoundFontsFolderURI(this)); - startActivity(intent); - }); - } else { - ViewGroup layout = (ViewGroup) button.getParent(); - if(layout != null) { - layout.removeView(button); - } - } + Helper.attachOpenFolderButton(this, button, SettingsManager.getSoundFontsFolderURI(this)); configureMusicVolume(); configureSoundVolume(); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java index f3546c162..baa373d9c 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java @@ -49,21 +49,7 @@ protected void onCreate(Bundle savedInstanceState) { // Setup UI components // The Font Button Button button = this.findViewById(R.id.button_open_font_folder); - // We can open the file picker in a specific folder only with API >= 26 - if (android.os.Build.VERSION.SDK_INT >= 26) { - button.setOnClickListener(v -> { - // Open the file explorer in the "fonts" folder - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.setType("*/*"); - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SettingsManager.getFontsFolderURI(this)); - startActivity(intent); - }); - } else { - ViewGroup layout = (ViewGroup) button.getParent(); - if(layout != null) { - layout.removeView(button); - } - } + Helper.attachOpenFolderButton(this, button, SettingsManager.getFontsFolderURI(this)); configureFont1Size(); configureFont2Size(); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java index 388041125..c43ddaacc 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java @@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatActivity; import org.easyrpg.player.BaseActivity; +import org.easyrpg.player.Helper; import org.easyrpg.player.R; import org.easyrpg.player.game_browser.GameBrowserActivity; import org.easyrpg.player.game_browser.GameBrowserHelper; @@ -41,39 +42,11 @@ public void onCreate(Bundle savedInstanceState) { // Setup UI components // The "Open Game Folder" Button Button openGameFolderButton = this.findViewById(R.id.open_game_folder); - // We can open the file picker in a specific folder only with API >= 26 - if (android.os.Build.VERSION.SDK_INT >= 26) { - openGameFolderButton.setOnClickListener(v -> { - // Open the file explorer in the "soundfont" folder - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.setType("*/*"); - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SettingsManager.getGamesFolderURI(this)); - startActivity(intent); - }); - } else { - ViewGroup layout = (ViewGroup) openGameFolderButton.getParent(); - if(layout != null) { - layout.removeView(openGameFolderButton); - } - } + Helper.attachOpenFolderButton(this, openGameFolderButton, SettingsManager.getGamesFolderURI(this)); // The "Open RTP Folder" Button Button openRTPFolderButton = this.findViewById(R.id.open_rtp_folder); - // We can open the file picker in a specific folder only with API >= 26 - if (android.os.Build.VERSION.SDK_INT >= 26) { - openRTPFolderButton.setOnClickListener(v -> { - // Open the file explorer in the "soundfont" folder - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.setType("*/*"); - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SettingsManager.getRTPFolderURI(this)); - startActivity(intent); - }); - } else { - ViewGroup layout = (ViewGroup) openRTPFolderButton.getParent(); - if(layout != null) { - layout.removeView(openRTPFolderButton); - } - } + Helper.attachOpenFolderButton(this, openRTPFolderButton, SettingsManager.getRTPFolderURI(this)); // Video button findViewById(R.id.watch_video).setOnClickListener(v -> { From 587e07169b1408feb1e973cc4553d4386d4ea5ef Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 20 Jan 2025 18:36:22 +0100 Subject: [PATCH 6/6] Android: Add a button to open the save directory --- .../org/easyrpg/player/game_browser/Game.java | 15 ++++++++++ .../game_browser/GameBrowserActivity.java | 28 +++++++++++++------ .../app/src/main/res/values/strings.xml | 1 + 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java index ce3df0971..7a0f2bce1 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 @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; +import org.easyrpg.player.Helper; import org.easyrpg.player.settings.SettingsManager; import java.io.ByteArrayOutputStream; @@ -173,6 +174,20 @@ public String toString() { return getDisplayTitle(); } + public Uri createSaveUri(Context context) { + if (!getSavePath().isEmpty()) { + DocumentFile saveFolder = Helper.createFolderInSave(context, getSavePath()); + + if (saveFolder != null) { + return saveFolder.getUri(); + } + } else { + return Uri.parse(getGameFolderPath()); + } + + return null; + } + public static Game fromCacheEntry(Context context, String cache) { String[] entries = cache.split(String.valueOf(escapeCode)); 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 41f6c0910..4b582f1fd 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,17 +15,14 @@ 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; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; @@ -36,10 +32,13 @@ import com.google.android.material.navigation.NavigationView; import org.easyrpg.player.BaseActivity; +import org.easyrpg.player.Helper; import org.easyrpg.player.R; import org.easyrpg.player.settings.SettingsManager; import org.libsdl.app.SDL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -343,22 +342,33 @@ public void onBindViewHolder(final ViewHolder holder, final int position) { // Settings Button holder.settingsButton.setOnClickListener(v -> { - String[] choices_list = { + ArrayList choices_list = new ArrayList(Arrays.asList( activity.getResources().getString(R.string.select_game_region), activity.getResources().getString(R.string.game_rename), activity.getResources().getString(R.string.launch_debug_mode) - }; + )); + + if (android.os.Build.VERSION.SDK_INT >= 26) { + choices_list.add(activity.getResources().getString(R.string.open_save_folder)); + } + + // It's 2025 and converting an ArrayList to an Array is still hot-garbage in Java + // because of type erasure and ugly APIs + String[] choices_list_arr = new String[choices_list.size()]; + choices_list.toArray(choices_list_arr); AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder .setTitle(R.string.settings) - .setItems(choices_list, (dialog, which) -> { + .setItems(choices_list_arr, (dialog, which) -> { if (which == 0) { - chooseRegion(activity, holder, gameList.get(position)); + chooseRegion(activity, holder, game); } else if (which == 1) { - renameGame(activity, holder, gameList.get(position)); + renameGame(activity, holder, game); } else if (which == 2) { launchGame(position, true); + } else if (which == 3) { + Helper.openFileBrowser(activity, game.createSaveUri(activity)); } }); 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 b8f992f08..74ff2345b 100644 --- a/builds/android/app/src/main/res/values/strings.xml +++ b/builds/android/app/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ Change the layout Launch in debug mode Rename game + Open savegame folder Choose a layout Unknown region Changing region failed