From dcb6040aadc327126b99fa6b8524b3f9d0c76989 Mon Sep 17 00:00:00 2001 From: Rafael Caetano Date: Sun, 21 Jun 2020 00:34:59 +0100 Subject: [PATCH] Add support for save states --- app/src/main/cpp/MelonDSAndroidJNI.cpp | 14 ++++++ .../me/magnum/melonds/MelonDSApplication.kt | 3 ++ .../java/me/magnum/melonds/MelonEmulator.kt | 6 +++ .../SharedPreferencesSettingsRepository.kt | 21 +++++++-- .../magnum/melonds/model/SaveStateLocation.kt | 7 +++ .../me/magnum/melonds/model/SaveStateSlot.kt | 5 ++ .../repositories/SettingsRepository.kt | 6 +-- .../melonds/ui/emulator/EmulatorActivity.kt | 47 +++++++++++++++++-- .../melonds/ui/emulator/EmulatorViewModel.kt | 33 +++++++++++++ app/src/main/res/values/arrays.xml | 6 +++ app/src/main/res/values/strings.xml | 15 ++++++ app/src/main/res/xml/pref_main.xml | 8 ++++ melonDS-android-lib | 2 +- 13 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/me/magnum/melonds/model/SaveStateLocation.kt create mode 100644 app/src/main/java/me/magnum/melonds/model/SaveStateSlot.kt create mode 100644 app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorViewModel.kt diff --git a/app/src/main/cpp/MelonDSAndroidJNI.cpp b/app/src/main/cpp/MelonDSAndroidJNI.cpp index 159790d..d5da20d 100644 --- a/app/src/main/cpp/MelonDSAndroidJNI.cpp +++ b/app/src/main/cpp/MelonDSAndroidJNI.cpp @@ -91,6 +91,20 @@ Java_me_magnum_melonds_MelonEmulator_resumeEmulation( JNIEnv* env, jclass type) MelonDSAndroid::resume(); } +JNIEXPORT jboolean JNICALL +Java_me_magnum_melonds_MelonEmulator_saveState( JNIEnv* env, jclass type, jstring path) +{ + const char* saveStatePath = path == nullptr ? nullptr : env->GetStringUTFChars(path, JNI_FALSE); + return MelonDSAndroid::saveState(saveStatePath); +} + +JNIEXPORT jboolean JNICALL +Java_me_magnum_melonds_MelonEmulator_loadState( JNIEnv* env, jclass type, jstring path) +{ + const char* saveStatePath = path == nullptr ? nullptr : env->GetStringUTFChars(path, JNI_FALSE); + return MelonDSAndroid::loadState(saveStatePath); +} + JNIEXPORT void JNICALL Java_me_magnum_melonds_MelonEmulator_stopEmulation( JNIEnv* env, jclass type) { diff --git a/app/src/main/java/me/magnum/melonds/MelonDSApplication.kt b/app/src/main/java/me/magnum/melonds/MelonDSApplication.kt index 72998f8..52ed1f0 100644 --- a/app/src/main/java/me/magnum/melonds/MelonDSApplication.kt +++ b/app/src/main/java/me/magnum/melonds/MelonDSApplication.kt @@ -12,6 +12,7 @@ import me.magnum.melonds.impl.FileSystemRomsRepository import me.magnum.melonds.impl.SharedPreferencesSettingsRepository import me.magnum.melonds.repositories.RomsRepository import me.magnum.melonds.repositories.SettingsRepository +import me.magnum.melonds.ui.emulator.EmulatorViewModel import me.magnum.melonds.ui.inputsetup.InputSetupViewModel import me.magnum.melonds.ui.romlist.RomListViewModel @@ -36,6 +37,8 @@ class MelonDSApplication : Application() { return RomListViewModel(ServiceLocator[RomsRepository::class], ServiceLocator[SettingsRepository::class]) as T if (modelClass == InputSetupViewModel::class.java) return InputSetupViewModel(ServiceLocator[SettingsRepository::class]) as T + if (modelClass == EmulatorViewModel::class.java) + return EmulatorViewModel(ServiceLocator[SettingsRepository::class]) as T throw RuntimeException("ViewModel of type " + modelClass.name + " is not supported") } diff --git a/app/src/main/java/me/magnum/melonds/MelonEmulator.kt b/app/src/main/java/me/magnum/melonds/MelonEmulator.kt index 7564c7b..4d57e59 100644 --- a/app/src/main/java/me/magnum/melonds/MelonEmulator.kt +++ b/app/src/main/java/me/magnum/melonds/MelonEmulator.kt @@ -43,6 +43,12 @@ object MelonEmulator { @JvmStatic external fun stopEmulation() + @JvmStatic + external fun saveState(path: String): Boolean + + @JvmStatic + external fun loadState(path: String): Boolean + @JvmStatic external fun onScreenTouch(x: Int, y: Int) diff --git a/app/src/main/java/me/magnum/melonds/impl/SharedPreferencesSettingsRepository.kt b/app/src/main/java/me/magnum/melonds/impl/SharedPreferencesSettingsRepository.kt index 2e22af9..0d9190b 100644 --- a/app/src/main/java/me/magnum/melonds/impl/SharedPreferencesSettingsRepository.kt +++ b/app/src/main/java/me/magnum/melonds/impl/SharedPreferencesSettingsRepository.kt @@ -9,10 +9,7 @@ import androidx.core.content.edit import com.google.gson.Gson import io.reactivex.Observable import io.reactivex.subjects.PublishSubject -import me.magnum.melonds.model.ControllerConfiguration -import me.magnum.melonds.model.SortingMode -import me.magnum.melonds.model.SortingOrder -import me.magnum.melonds.model.VideoFiltering +import me.magnum.melonds.model.* import me.magnum.melonds.repositories.SettingsRepository import me.magnum.melonds.ui.Theme import me.magnum.melonds.utils.PreferenceDirectoryUtils @@ -90,6 +87,22 @@ class SharedPreferencesSettingsRepository(private val context: Context, private return PreferenceDirectoryUtils.getSingleDirectoryFromPreference(dirPreference) } + override fun getSaveStateDirectory(rom: Rom): String { + val locationPreference = preferences.getString("save_state_location", "save_dir")!! + val saveStateLocation = SaveStateLocation.valueOf(locationPreference.toUpperCase(Locale.ROOT)) + + return when (saveStateLocation) { + SaveStateLocation.SAVE_DIR -> { + if (saveNextToRomFile()) + File(rom.path).parentFile!!.absolutePath + else + getSaveFileDirectory() ?: File(rom.path).parentFile!!.absolutePath + } + SaveStateLocation.ROM_DIR -> File(rom.path).parentFile!!.absolutePath + SaveStateLocation.INTERNAL_DIR -> File(context.getExternalFilesDir(null), "savestates").absolutePath + } + } + override fun getControllerConfiguration(): ControllerConfiguration { if (controllerConfiguration == null) { try { diff --git a/app/src/main/java/me/magnum/melonds/model/SaveStateLocation.kt b/app/src/main/java/me/magnum/melonds/model/SaveStateLocation.kt new file mode 100644 index 0000000..c391ace --- /dev/null +++ b/app/src/main/java/me/magnum/melonds/model/SaveStateLocation.kt @@ -0,0 +1,7 @@ +package me.magnum.melonds.model + +enum class SaveStateLocation { + SAVE_DIR, + ROM_DIR, + INTERNAL_DIR +} \ No newline at end of file diff --git a/app/src/main/java/me/magnum/melonds/model/SaveStateSlot.kt b/app/src/main/java/me/magnum/melonds/model/SaveStateSlot.kt new file mode 100644 index 0000000..568ba08 --- /dev/null +++ b/app/src/main/java/me/magnum/melonds/model/SaveStateSlot.kt @@ -0,0 +1,5 @@ +package me.magnum.melonds.model + +import java.util.* + +data class SaveStateSlot(val slot: Int, val exists: Boolean, val path: String, val lastUsedDate: Date?) \ No newline at end of file diff --git a/app/src/main/java/me/magnum/melonds/repositories/SettingsRepository.kt b/app/src/main/java/me/magnum/melonds/repositories/SettingsRepository.kt index a9110f2..11b57fb 100644 --- a/app/src/main/java/me/magnum/melonds/repositories/SettingsRepository.kt +++ b/app/src/main/java/me/magnum/melonds/repositories/SettingsRepository.kt @@ -1,10 +1,7 @@ package me.magnum.melonds.repositories import io.reactivex.Observable -import me.magnum.melonds.model.ControllerConfiguration -import me.magnum.melonds.model.SortingMode -import me.magnum.melonds.model.SortingOrder -import me.magnum.melonds.model.VideoFiltering +import me.magnum.melonds.model.* import me.magnum.melonds.ui.Theme interface SettingsRepository { @@ -21,6 +18,7 @@ interface SettingsRepository { fun getRomSortingOrder(): SortingOrder fun saveNextToRomFile(): Boolean fun getSaveFileDirectory(): String? + fun getSaveStateDirectory(rom: Rom): String fun getControllerConfiguration(): ControllerConfiguration fun showSoftInput(): Boolean diff --git a/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorActivity.kt b/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorActivity.kt index 2e1ede8..6b58a3b 100644 --- a/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorActivity.kt +++ b/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorActivity.kt @@ -8,9 +8,12 @@ import android.view.View import android.view.Window import android.widget.RelativeLayout import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.os.ConfigurationCompat +import androidx.lifecycle.ViewModelProvider import io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers @@ -21,10 +24,7 @@ import me.magnum.melonds.MelonEmulator import me.magnum.melonds.MelonEmulator.LoadResult import me.magnum.melonds.R import me.magnum.melonds.ServiceLocator -import me.magnum.melonds.model.Input -import me.magnum.melonds.model.RendererConfiguration -import me.magnum.melonds.model.Rom -import me.magnum.melonds.model.RomConfig +import me.magnum.melonds.model.* import me.magnum.melonds.parcelables.RomParcelable import me.magnum.melonds.repositories.RomsRepository import me.magnum.melonds.repositories.SettingsRepository @@ -33,6 +33,7 @@ import me.magnum.melonds.ui.emulator.DSRenderer.RendererListener import me.magnum.melonds.ui.input.* import java.io.File import java.nio.ByteBuffer +import java.text.SimpleDateFormat class EmulatorActivity : AppCompatActivity(), RendererListener { companion object { @@ -49,9 +50,13 @@ class EmulatorActivity : AppCompatActivity(), RendererListener { private enum class PauseMenuOptions(val textResource: Int) { SETTINGS(R.string.settings), + SAVE_STATE(R.string.save_state), + LOAD_STATE(R.string.load_state), EXIT(R.string.exit); } + private val viewModel: EmulatorViewModel by viewModels { ServiceLocator[ViewModelProvider.Factory::class] } + private lateinit var loadedRom: Rom private lateinit var dsRenderer: DSRenderer private lateinit var romsRepository: RomsRepository private lateinit var settingsRepository: SettingsRepository @@ -160,6 +165,7 @@ class EmulatorActivity : AppCompatActivity(), RendererListener { } romLoader.flatMap { + loadedRom = it Single.create { emitter -> MelonEmulator.setupEmulator(getConfigDirPath(), assets) @@ -256,6 +262,22 @@ class EmulatorActivity : AppCompatActivity(), RendererListener { val settingsIntent = Intent(this@EmulatorActivity, SettingsActivity::class.java) startActivityForResult(settingsIntent, REQUEST_SETTINGS) } + PauseMenuOptions.SAVE_STATE -> pickSaveStateSlot { + if (!MelonEmulator.saveState(it.path)) + Toast.makeText(this@EmulatorActivity, getString(R.string.failed_save_state), Toast.LENGTH_SHORT).show() + + MelonEmulator.resumeEmulation() + } + PauseMenuOptions.LOAD_STATE -> pickSaveStateSlot { + if (!it.exists) { + Toast.makeText(this@EmulatorActivity, getString(R.string.cant_load_empty_slot), Toast.LENGTH_SHORT).show() + } else { + if (!MelonEmulator.loadState(it.path)) + Toast.makeText(this@EmulatorActivity, getString(R.string.failed_load_state), Toast.LENGTH_SHORT).show() + } + + MelonEmulator.resumeEmulation() + } PauseMenuOptions.EXIT -> finish() } } @@ -328,6 +350,23 @@ class EmulatorActivity : AppCompatActivity(), RendererListener { runOnUiThread { textFps.text = getString(R.string.info_fps, fps) } } + private fun pickSaveStateSlot(onSlotPicked: (SaveStateSlot) -> Unit) { + val dateFormatter = SimpleDateFormat("EEE, dd MMMM yyyy kk:mm:ss", ConfigurationCompat.getLocales(resources.configuration)[0]) + val slots = viewModel.getRomSaveStateSlots(loadedRom) + val options = slots.map { "${it.slot}. ${if (it.exists) dateFormatter.format(it.lastUsedDate!!) else getString(R.string.empty_slot)}" }.toTypedArray() + + AlertDialog.Builder(this) + .setTitle(getString(R.string.save_slot)) + .setItems(options) { _, which -> + onSlotPicked(slots[which]) + } + .setNegativeButton(R.string.cancel) { dialog, _ -> + dialog.cancel() + } + .setOnCancelListener { MelonEmulator.resumeEmulation() } + .show() + } + override fun onPause() { super.onPause() surfaceMain.onPause() diff --git a/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorViewModel.kt b/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorViewModel.kt new file mode 100644 index 0000000..7a50796 --- /dev/null +++ b/app/src/main/java/me/magnum/melonds/ui/emulator/EmulatorViewModel.kt @@ -0,0 +1,33 @@ +package me.magnum.melonds.ui.emulator + +import androidx.lifecycle.ViewModel +import me.magnum.melonds.model.Rom +import me.magnum.melonds.model.SaveStateSlot +import me.magnum.melonds.repositories.SettingsRepository +import java.io.File +import java.util.* + +class EmulatorViewModel(private val settingsRepository: SettingsRepository) : ViewModel() { + fun getRomSaveStateSlots(rom: Rom): List { + val saveStatePath = settingsRepository.getSaveStateDirectory(rom) + val saveStateDirectory = File(saveStatePath) + if (!saveStateDirectory.isDirectory) { + // If the directory cannot be created, there's no point in returning slots + if (!saveStateDirectory.mkdirs()) + return emptyList() + } + + val romFileName = File(rom.path).nameWithoutExtension + + val saveStateSlots = mutableListOf() + for (i in 1..8) { + val saveStateFile = File(saveStateDirectory, "$romFileName.ml$i") + if (saveStateFile.isFile) + saveStateSlots.add(SaveStateSlot(i, true, saveStateFile.absolutePath, Date(saveStateFile.lastModified()))) + else + saveStateSlots.add(SaveStateSlot(i, false, saveStateFile.absolutePath, null)) + } + + return saveStateSlots + } +} \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 7928f6e..af0a3a0 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -10,4 +10,10 @@ dark system + + + save_dir + rom_dir + internal_dir + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bfb523..06e407c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ OK No + Cancel Not set Could not load ROM @@ -15,6 +16,9 @@ Search ROMs… FPS: %1$d LOADING… + Failed to save state + Failed to load state + Can\'t load an empty save state slot Alphabetically Recently played @@ -34,6 +38,10 @@ Exit Settings + Save state + Load state + Save slot + <Empty> Settings General System @@ -46,6 +54,7 @@ Save Files Save next to ROM file Save file directory + Save state location Input Key mapping Show soft input @@ -84,4 +93,10 @@ None Linear + + + Save file directory + ROM directory + Internal directory + diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 1044992..002bf1b 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -71,6 +71,14 @@ app:root_dir="/sdcard" app:selection_mode="single_mode" app:selection_type="dir_select" /> + +