Add support for save states

This commit is contained in:
Rafael Caetano 2020-06-21 00:34:59 +01:00
parent cf96f724ba
commit dcb6040aad
13 changed files with 160 additions and 13 deletions

View File

@ -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)
{

View File

@ -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")
}

View File

@ -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)

View File

@ -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 {

View File

@ -0,0 +1,7 @@
package me.magnum.melonds.model
enum class SaveStateLocation {
SAVE_DIR,
ROM_DIR,
INTERNAL_DIR
}

View File

@ -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?)

View File

@ -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

View File

@ -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<LoadResult> { 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()

View File

@ -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<SaveStateSlot> {
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<SaveStateSlot>()
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
}
}

View File

@ -10,4 +10,10 @@
<item>dark</item>
<item>system</item>
</string-array>
<string-array name="save_state_location_values">
<item>save_dir</item>
<item>rom_dir</item>
<item>internal_dir</item>
</string-array>
</resources>

View File

@ -3,6 +3,7 @@
<string name="ok">OK</string>
<string name="no">No</string>
<string name="cancel">Cancel</string>
<string name="not_set">Not set</string>
<string name="error_load_rom">Could not load ROM</string>
@ -15,6 +16,9 @@
<string name="hint_search_roms">Search ROMs…</string>
<string name="info_fps">FPS: %1$d</string>
<string name="info_loading">LOADING…</string>
<string name="failed_save_state">Failed to save state</string>
<string name="failed_load_state">Failed to load state</string>
<string name="cant_load_empty_slot">Can\'t load an empty save state slot</string>
<string name="action_sort_alphabetically">Alphabetically</string>
<string name="action_sort_recently_played">Recently played</string>
@ -34,6 +38,10 @@
<string name="exit">Exit</string>
<string name="settings">Settings</string>
<string name="save_state">Save state</string>
<string name="load_state">Load state</string>
<string name="save_slot">Save slot</string>
<string name="empty_slot">&lt;Empty&gt;</string>
<string name="title_activity_settings">Settings</string>
<string name="category_general">General</string>
<string name="category_system">System</string>
@ -46,6 +54,7 @@
<string name="category_save_files">Save Files</string>
<string name="save_next_rom">Save next to ROM file</string>
<string name="save_file_directory">Save file directory</string>
<string name="save_state_location">Save state location</string>
<string name="input">Input</string>
<string name="key_mapping">Key mapping</string>
<string name="show_soft_input">Show soft input</string>
@ -84,4 +93,10 @@
<item>None</item>
<item>Linear</item>
</string-array>
<string-array name="save_state_locations">
<item>Save file directory</item>
<item>ROM directory</item>
<item>Internal directory</item>
</string-array>
</resources>

View File

@ -71,6 +71,14 @@
app:root_dir="/sdcard"
app:selection_mode="single_mode"
app:selection_type="dir_select" />
<ListPreference
android:key="save_state_location"
android:title="@string/save_state_location"
android:summary="%s"
android:entries="@array/save_state_locations"
android:entryValues="@array/save_state_location_values"
android:defaultValue="save_dir" />
</PreferenceCategory>
<PreferenceCategory

@ -1 +1 @@
Subproject commit 87d8b6d47a59e8b4ddb00753fbf0e78abea44692
Subproject commit c70176f1e3c297d35b20b0fc1dd4ee09503e44e3