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 a9d75ed0e..a02a7ae5e 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(); auto ge_list = FileFinder::FindGames(root); @@ -196,7 +200,7 @@ Java_org_easyrpg_player_game_1browser_GameScanner_findGames(JNIEnv *env, jclass, if (ge_list.empty()) { // No games found - return jgame_array; + return nullptr; } jmethodID jgame_constructor_unsupported = env->GetMethodID(jgame_class, "", "(I)V"); 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/Game.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/Game.java index d9c67217c..800501bc6 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; @@ -188,6 +189,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 2206c522f..746c885af 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 @@ -23,7 +23,6 @@ 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; @@ -33,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; @@ -358,22 +360,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/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/game_browser/GameScanner.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java index 543c77f0d..687f388d6 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/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 -> { diff --git a/builds/android/app/src/main/res/values/strings.xml b/builds/android/app/src/main/res/values/strings.xml index 3ccec5612..fcf472fa4 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 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; diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 053162937..e086b2417 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -878,7 +878,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_map.cpp b/src/game_map.cpp index 3acdc4bbf..8b4ab6507 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -650,7 +650,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; } @@ -1665,7 +1667,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) { @@ -1687,7 +1691,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) { diff --git a/src/game_player.cpp b/src/game_player.cpp index 40da7dbff..1043937d1 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -429,7 +429,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; } @@ -445,7 +445,7 @@ bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_d && trigger >= 0 && triggers[trigger]) { SetEncounterCalling(false); - const auto triggered = ev.ScheduleForegroundExecution(triggered_by_decision_key, true); + const auto triggered = ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); result |= triggered; if (triggered) { GMI().MainPlayerTriggeredEvent(ev.GetId(), triggered_by_decision_key); @@ -455,7 +455,7 @@ bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_d 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; } @@ -470,7 +470,7 @@ bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool && trigger >= 0 && triggers[trigger]) { SetEncounterCalling(false); - const auto triggered = ev.ScheduleForegroundExecution(triggered_by_decision_key, true); + const auto triggered = ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); result |= triggered; if (triggered) { GMI().MainPlayerTriggeredEvent(ev.GetId(), triggered_by_decision_key); @@ -937,6 +937,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(); 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);