mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-23 09:59:39 -04:00
feat(android): add custom dump location selection for RomFS/ExeFS
Allow users to specify a custom directory for dumping game files via the Android document picker, with fallback to default dump directory. Adds optional dumpPath parameter to both dump functions and UI dialog for location selection. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -470,12 +470,14 @@ object NativeLibrary {
|
||||
* Dumps the RomFS from a game to the dump directory
|
||||
* @param gamePath Path to the game file
|
||||
* @param programId String representation of the game's program ID
|
||||
* @param dumpPath Optional custom dump path. If null or empty, uses default dump directory
|
||||
* @param callback Progress callback. Return true to cancel. Parameters: (max: Long, progress: Long)
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
external fun dumpRomFS(
|
||||
gamePath: String,
|
||||
programId: String,
|
||||
dumpPath: String?,
|
||||
callback: (max: Long, progress: Long) -> Boolean
|
||||
): Boolean
|
||||
|
||||
@@ -483,12 +485,14 @@ object NativeLibrary {
|
||||
* Dumps the ExeFS from a game to the dump directory
|
||||
* @param gamePath Path to the game file
|
||||
* @param programId String representation of the game's program ID
|
||||
* @param dumpPath Optional custom dump path. If null or empty, uses default dump directory
|
||||
* @param callback Progress callback. Return true to cancel. Parameters: (max: Long, progress: Long)
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
external fun dumpExeFS(
|
||||
gamePath: String,
|
||||
programId: String,
|
||||
dumpPath: String?,
|
||||
callback: (max: Long, progress: Long) -> Boolean
|
||||
): Boolean
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.citron.citron_emu.model.InstallableProperty
|
||||
import org.citron.citron_emu.model.SubmenuProperty
|
||||
import org.citron.citron_emu.model.TaskState
|
||||
import org.citron.citron_emu.utils.DirectoryInitialization
|
||||
import org.citron.citron_emu.utils.DocumentsTree
|
||||
import org.citron.citron_emu.utils.FileUtil
|
||||
import org.citron.citron_emu.utils.GameIconUtils
|
||||
import org.citron.citron_emu.utils.GpuDriverHelper
|
||||
@@ -47,6 +48,7 @@ import org.citron.citron_emu.utils.MemoryUtil
|
||||
import org.citron.citron_emu.utils.ViewUtils.marquee
|
||||
import org.citron.citron_emu.utils.ViewUtils.updateMargins
|
||||
import org.citron.citron_emu.utils.collect
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
@@ -283,25 +285,23 @@ class GamePropertiesFragment : Fragment() {
|
||||
R.string.dump_romfs_description,
|
||||
R.drawable.ic_save
|
||||
) {
|
||||
ProgressDialogFragment.newInstance(
|
||||
// Show dialog to select dump location or use default
|
||||
MessageDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.dump_romfs_extracting,
|
||||
false
|
||||
) { _, _ ->
|
||||
val success = NativeLibrary.dumpRomFS(
|
||||
args.game.path,
|
||||
args.game.programIdHex,
|
||||
{ max, progress ->
|
||||
// Progress callback - return true to cancel
|
||||
false
|
||||
}
|
||||
)
|
||||
if (success) {
|
||||
getString(R.string.dump_success)
|
||||
} else {
|
||||
getString(R.string.dump_failed)
|
||||
titleId = R.string.dump_romfs,
|
||||
descriptionId = R.string.select_dump_location_description,
|
||||
positiveButtonTitleId = R.string.select_location,
|
||||
negativeButtonTitleId = R.string.use_default_location,
|
||||
positiveAction = {
|
||||
// User wants to select a custom location
|
||||
pendingDumpType = "romfs"
|
||||
selectDumpDirectory.launch(null)
|
||||
},
|
||||
negativeAction = {
|
||||
// Use default location
|
||||
performRomFSDump(null)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -311,25 +311,23 @@ class GamePropertiesFragment : Fragment() {
|
||||
R.string.dump_exefs_description,
|
||||
R.drawable.ic_save
|
||||
) {
|
||||
ProgressDialogFragment.newInstance(
|
||||
// Show dialog to select dump location or use default
|
||||
MessageDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.dump_exefs_extracting,
|
||||
false
|
||||
) { _, _ ->
|
||||
val success = NativeLibrary.dumpExeFS(
|
||||
args.game.path,
|
||||
args.game.programIdHex,
|
||||
{ max, progress ->
|
||||
// Progress callback - return true to cancel
|
||||
false
|
||||
}
|
||||
)
|
||||
if (success) {
|
||||
getString(R.string.dump_success)
|
||||
} else {
|
||||
getString(R.string.dump_failed)
|
||||
titleId = R.string.dump_exefs,
|
||||
descriptionId = R.string.select_dump_location_description,
|
||||
positiveButtonTitleId = R.string.select_location,
|
||||
negativeButtonTitleId = R.string.use_default_location,
|
||||
positiveAction = {
|
||||
// User wants to select a custom location
|
||||
pendingDumpType = "exefs"
|
||||
selectDumpDirectory.launch(null)
|
||||
},
|
||||
negativeAction = {
|
||||
// Use default location
|
||||
performExeFSDump(null)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -387,6 +385,22 @@ class GamePropertiesFragment : Fragment() {
|
||||
windowInsets
|
||||
}
|
||||
|
||||
private var pendingDumpType: String? = null // "romfs" or "exefs"
|
||||
|
||||
private val selectDumpDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
// Store the selected directory URI and perform the dump
|
||||
val selectedUri = result.toString()
|
||||
when (pendingDumpType) {
|
||||
"romfs" -> performRomFSDump(selectedUri)
|
||||
"exefs" -> performExeFSDump(selectedUri)
|
||||
}
|
||||
pendingDumpType = null
|
||||
}
|
||||
|
||||
private val importSaves =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result == null) {
|
||||
@@ -480,4 +494,87 @@ class GamePropertiesFragment : Fragment() {
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private fun performRomFSDump(dumpPathUri: String?) {
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.dump_romfs_extracting,
|
||||
false
|
||||
) { _, _ ->
|
||||
// Convert URI to file path if needed
|
||||
val dumpPathString = dumpPathUri?.let { uriString ->
|
||||
try {
|
||||
val uri = android.net.Uri.parse(uriString)
|
||||
// For document tree URIs, try to get the actual file path
|
||||
if (DocumentsTree.isNativePath(uriString)) {
|
||||
uriString
|
||||
} else {
|
||||
// Try to extract file path from document URI
|
||||
// For document tree URIs, we can't easily get a native path
|
||||
// So we'll pass the URI and let the native code handle it
|
||||
// or extract path using DocumentFile
|
||||
val docFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
docFile?.uri?.path ?: uriString
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val success = NativeLibrary.dumpRomFS(
|
||||
args.game.path,
|
||||
args.game.programIdHex,
|
||||
dumpPathString,
|
||||
{ max, progress ->
|
||||
// Progress callback - return true to cancel
|
||||
false
|
||||
}
|
||||
)
|
||||
if (success) {
|
||||
getString(R.string.dump_success)
|
||||
} else {
|
||||
getString(R.string.dump_failed)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private fun performExeFSDump(dumpPathUri: String?) {
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.dump_exefs_extracting,
|
||||
false
|
||||
) { _, _ ->
|
||||
// Convert URI to file path if needed
|
||||
val dumpPathString = dumpPathUri?.let { uriString ->
|
||||
try {
|
||||
val uri = android.net.Uri.parse(uriString)
|
||||
// For document tree URIs, try to get the actual file path
|
||||
if (DocumentsTree.isNativePath(uriString)) {
|
||||
uriString
|
||||
} else {
|
||||
// Try to extract file path from document URI
|
||||
val docFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
docFile?.uri?.path ?: uriString
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val success = NativeLibrary.dumpExeFS(
|
||||
args.game.path,
|
||||
args.game.programIdHex,
|
||||
dumpPathString,
|
||||
{ max, progress ->
|
||||
// Progress callback - return true to cancel
|
||||
false
|
||||
}
|
||||
)
|
||||
if (success) {
|
||||
getString(R.string.dump_success)
|
||||
} else {
|
||||
getString(R.string.dump_failed)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,7 +883,7 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, j
|
||||
|
||||
jboolean Java_org_citron_citron_1emu_NativeLibrary_dumpRomFS(JNIEnv* env, jobject jobj,
|
||||
jstring jgamePath, jstring jprogramId,
|
||||
jobject jcallback) {
|
||||
jstring jdumpPath, jobject jcallback) {
|
||||
const auto game_path = Common::Android::GetJString(env, jgamePath);
|
||||
const auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
|
||||
@@ -935,7 +935,25 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_dumpRomFS(JNIEnv* env, jobjec
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto dump_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::DumpDir);
|
||||
// Use custom dump path if provided, otherwise use default
|
||||
std::filesystem::path dump_dir;
|
||||
if (jdumpPath != nullptr) {
|
||||
const auto custom_path = Common::Android::GetJString(env, jdumpPath);
|
||||
if (!custom_path.empty()) {
|
||||
// Check if it's a native path (starts with /) or try to use it as-is
|
||||
if (custom_path[0] == '/') {
|
||||
dump_dir = std::filesystem::path(custom_path);
|
||||
} else {
|
||||
// Try to parse as URI and extract path
|
||||
// For document tree URIs, we can't easily get a native path
|
||||
// So fall back to default
|
||||
dump_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::DumpDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dump_dir.empty()) {
|
||||
dump_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::DumpDir);
|
||||
}
|
||||
const auto romfs_dir = fmt::format("{:016X}/romfs", title_id);
|
||||
const auto path = dump_dir / romfs_dir;
|
||||
|
||||
@@ -1014,7 +1032,7 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_dumpRomFS(JNIEnv* env, jobjec
|
||||
|
||||
jboolean Java_org_citron_citron_1emu_NativeLibrary_dumpExeFS(JNIEnv* env, jobject jobj,
|
||||
jstring jgamePath, jstring jprogramId,
|
||||
jobject jcallback) {
|
||||
jstring jdumpPath, jobject jcallback) {
|
||||
const auto game_path = Common::Android::GetJString(env, jgamePath);
|
||||
const auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
|
||||
@@ -1069,10 +1087,25 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_dumpExeFS(JNIEnv* env, jobjec
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get dump directory
|
||||
const auto dump_dir = system.GetFileSystemController().GetModificationDumpRoot(title_id);
|
||||
// Use custom dump path if provided, otherwise use default
|
||||
FileSys::VirtualDir dump_dir;
|
||||
if (jdumpPath != nullptr) {
|
||||
const auto custom_path = Common::Android::GetJString(env, jdumpPath);
|
||||
if (!custom_path.empty() && custom_path[0] == '/') {
|
||||
// Create a real VFS directory from the custom path (native path only)
|
||||
const auto custom_dir = std::make_shared<FileSys::RealVfsDirectory>(
|
||||
std::filesystem::path(custom_path));
|
||||
if (custom_dir && custom_dir->IsWritable()) {
|
||||
dump_dir = custom_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dump_dir) {
|
||||
return false;
|
||||
// Fall back to default dump directory
|
||||
dump_dir = system.GetFileSystemController().GetModificationDumpRoot(title_id);
|
||||
if (!dump_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const auto exefs_dir = FileSys::GetOrCreateDirectoryRelative(dump_dir, "/exefs");
|
||||
|
||||
@@ -153,6 +153,9 @@
|
||||
<string name="dump_exefs_extracting">Extracting ExeFS…</string>
|
||||
<string name="dump_success">Extraction completed successfully</string>
|
||||
<string name="dump_failed">Extraction failed</string>
|
||||
<string name="select_dump_location_description">Choose where to save the extracted files. Select a location or use the default dump directory.</string>
|
||||
<string name="select_location">Select Location</string>
|
||||
<string name="use_default_location">Use Default</string>
|
||||
|
||||
<!-- Applet launcher strings -->
|
||||
<string name="applets">Applet launcher</string>
|
||||
|
||||
Reference in New Issue
Block a user