Skip to content

Commit

Permalink
Add buttons to quick save and load the state
Browse files Browse the repository at this point in the history
* Allows new buttons to be mapped to the controller
* Allows new soft buttons to be added to custom layouts
  • Loading branch information
rafaelvcaetano committed Oct 16, 2021
1 parent 3ab08a3 commit 3b8c582
Show file tree
Hide file tree
Showing 19 changed files with 171 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ class ControllerConfiguration(configList: List<InputConfig>) {
Input.PAUSE,
Input.FAST_FORWARD,
Input.RESET,
Input.SWAP_SCREENS
Input.SWAP_SCREENS,
Input.QUICK_SAVE,
Input.QUICK_LOAD
)

fun empty(): ControllerConfiguration {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/me/magnum/melonds/domain/model/Input.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ enum class Input(val keyCode: Int) {
FAST_FORWARD(-1),
RESET(-1),
TOGGLE_SOFT_INPUT(-1),
SWAP_SCREENS(-1);
SWAP_SCREENS(-1),
QUICK_SAVE(-1),
QUICK_LOAD(-1);

val isSystemInput: Boolean
get() = keyCode != -1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ enum class LayoutComponent {
BUTTON_TOGGLE_SOFT_INPUT,
BUTTON_RESET,
BUTTON_PAUSE,
BUTTON_SWAP_SCREENS;
BUTTON_SWAP_SCREENS,
BUTTON_QUICK_SAVE,
BUTTON_QUICK_LOAD;

fun isScreen(): Boolean {
return this == TOP_SCREEN || this == BOTTOM_SCREEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ package me.magnum.melonds.domain.model
import android.net.Uri
import java.util.*

data class SaveStateSlot(val slot: Int, val exists: Boolean, val lastUsedDate: Date?, val screenshot: Uri?)
data class SaveStateSlot(val slot: Int, val exists: Boolean, val lastUsedDate: Date?, val screenshot: Uri?) {

companion object {
const val QUICK_SAVE_SLOT = 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import me.magnum.melonds.domain.model.SaveStateSlot

interface SaveStatesRepository {
fun getRomSaveStates(rom: Rom): List<SaveStateSlot>
fun getRomQuickSaveStateSlot(rom: Rom): SaveStateSlot
fun getRomSaveStateUri(rom: Rom, saveState: SaveStateSlot): Uri
fun setRomSaveStateScreenshot(rom: Rom, saveState: SaveStateSlot, screenshot: Bitmap)
fun deleteRomSaveState(rom: Rom, saveState: SaveStateSlot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package me.magnum.melonds.impl

import android.graphics.Bitmap
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.SaveStateSlot
import me.magnum.melonds.domain.repositories.SaveStatesRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.extensions.nameWithoutExtension
import me.magnum.melonds.ui.emulator.exceptions.SaveSlotLoadException
import java.util.*

Expand All @@ -17,34 +19,39 @@ class FileSystemSaveStatesRepository(
) : SaveStatesRepository {

override fun getRomSaveStates(rom: Rom): List<SaveStateSlot> {
val saveStateDirectoryUri = settingsRepository.getSaveStateDirectory(rom) ?: return emptyList()
val saveStateDirectoryDocument = uriHandler.getUriTreeDocument(saveStateDirectoryUri) ?: return emptyList()
val romDocument = uriHandler.getUriDocument(rom.uri)!!
val romFileName = romDocument.name?.substringBeforeLast('.') ?: return emptyList()
val saveStateDirectoryDocument = getSaveStateDirectoryDocument(rom) ?: return emptyList()
val romFileName = getRomFileNameWithoutExtension(rom) ?: return emptyList()

val saveStateSlots = Array(8) {
SaveStateSlot(it + 1, false, null, null)
val saveStateSlots = Array(9) {
SaveStateSlot(it, false, null, null)
}
val fileNameRegex = "${Regex.escape(romFileName)}\\.ml[1-8]".toRegex()
val fileNameRegex = "${Regex.escape(romFileName)}\\.ml[0-8]".toRegex()
saveStateDirectoryDocument.listFiles().forEach {
val fileName = it.name
if (fileName?.matches(fileNameRegex) == true) {
val slotNumber = fileName.last().digitToInt()
val slot = SaveStateSlot(slotNumber, true, Date(it.lastModified()), null)
val screenshotUri = saveStateScreenshotProvider.getRomSaveStateScreenshotUri(rom, slot)
saveStateSlots[slotNumber - 1] = slot.copy(screenshot = screenshotUri)
saveStateSlots[slotNumber] = slot.copy(screenshot = screenshotUri)
}
}

return saveStateSlots.toList()
}

override fun getRomQuickSaveStateSlot(rom: Rom): SaveStateSlot {
val quickSaveStateDocument = getRomQuickSaveStateDocument(rom)
val saveStateExists = quickSaveStateDocument != null
val lastModified = quickSaveStateDocument?.let { Date(it.lastModified()) }
val slot = SaveStateSlot(SaveStateSlot.QUICK_SAVE_SLOT, saveStateExists, lastModified, null)
val screenshotUri = saveStateScreenshotProvider.getRomSaveStateScreenshotUri(rom, slot)
return slot.copy(screenshot = screenshotUri)
}

override fun getRomSaveStateUri(rom: Rom, saveState: SaveStateSlot): Uri {
val saveStateDirectoryUri = settingsRepository.getSaveStateDirectory(rom) ?: throw SaveSlotLoadException("Could not determine save slot parent directory")
val saveStateDirectoryDocument = uriHandler.getUriTreeDocument(saveStateDirectoryUri) ?: throw SaveSlotLoadException("Could not create parent directory document")
val saveStateDirectoryDocument = getSaveStateDirectoryDocument(rom) ?: throw SaveSlotLoadException("Could not create parent directory document")

val romDocument = uriHandler.getUriDocument(rom.uri) ?: throw SaveSlotLoadException("Could not create ROM document")
val romFileName = romDocument.name?.substringBeforeLast('.') ?: throw SaveSlotLoadException("Could not determine ROM file name")
val romFileName = getRomFileNameWithoutExtension(rom) ?: throw SaveSlotLoadException("Could not determine ROM file name")
val saveStateName = "$romFileName.ml${saveState.slot}"
val saveStateFile = saveStateDirectoryDocument.findFile(saveStateName)

Expand All @@ -66,15 +73,31 @@ class FileSystemSaveStatesRepository(
return
}

val saveStateDirectoryUri = settingsRepository.getSaveStateDirectory(rom) ?: throw SaveSlotLoadException("Could not determine save slot parent directory")
val saveStateDirectoryDocument = uriHandler.getUriTreeDocument(saveStateDirectoryUri) ?: throw SaveSlotLoadException("Could not create parent directory document")
val saveStateDirectoryDocument = getSaveStateDirectoryDocument(rom) ?: throw SaveSlotLoadException("Could not create parent directory document")
val romFileName = getRomFileNameWithoutExtension(rom) ?: throw SaveSlotLoadException("Could not determine ROM file name")

val romDocument = uriHandler.getUriDocument(rom.uri) ?: throw SaveSlotLoadException("Could not create ROM document")
val romFileName = romDocument.name?.substringBeforeLast('.') ?: throw SaveSlotLoadException("Could not determine ROM file name")
val saveStateName = "$romFileName.ml${saveState.slot}"
val saveStateFile = saveStateDirectoryDocument.findFile(saveStateName)

saveStateFile?.delete()
saveStateScreenshotProvider.deleteRomSaveStateScreenshot(rom, saveState)
}

private fun getRomQuickSaveStateDocument(rom: Rom): DocumentFile? {
val saveStateDirectoryDocument = getSaveStateDirectoryDocument(rom) ?: return null
val romFileName = getRomFileNameWithoutExtension(rom) ?: return null

val quickSaveStateFileName = "$romFileName.ml0"
return saveStateDirectoryDocument.findFile(quickSaveStateFileName)
}

private fun getSaveStateDirectoryDocument(rom: Rom): DocumentFile? {
val saveStateDirectoryUri = settingsRepository.getSaveStateDirectory(rom) ?: return null
return uriHandler.getUriTreeDocument(saveStateDirectoryUri)
}

private fun getRomFileNameWithoutExtension(rom: Rom): String? {
val romDocument = uriHandler.getUriDocument(rom.uri)
return romDocument?.nameWithoutExtension
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class SingleButtonLayoutComponentViewBuilder(private val layoutComponent: Layout
LayoutComponent.BUTTON_TOGGLE_SOFT_INPUT -> R.drawable.ic_touch_enabled
LayoutComponent.BUTTON_RESET -> R.drawable.button_reset
LayoutComponent.BUTTON_SWAP_SCREENS -> R.drawable.button_swap_screens
LayoutComponent.BUTTON_QUICK_SAVE -> R.drawable.button_quick_save
LayoutComponent.BUTTON_QUICK_LOAD -> R.drawable.button_quick_load
else -> -1
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
override fun onSwapScreens() {
swapScreen()
}

override fun onQuickSave() {
performQuickSave()
}

override fun onQuickLoad() {
performQuickLoad()
}
}
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val newEmulatorConfiguration = delegate.getEmulatorConfiguration()
Expand Down Expand Up @@ -367,6 +375,8 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
binding.viewLayoutControls.getLayoutComponentView(LayoutComponent.BUTTON_FAST_FORWARD_TOGGLE)?.view?.setOnTouchListener(SingleButtonInputHandler(frontendInputHandler, Input.FAST_FORWARD, enableHapticFeedback, touchVibrator))
binding.viewLayoutControls.getLayoutComponentView(LayoutComponent.BUTTON_TOGGLE_SOFT_INPUT)?.view?.setOnTouchListener(SingleButtonInputHandler(frontendInputHandler, Input.TOGGLE_SOFT_INPUT, enableHapticFeedback, touchVibrator))
binding.viewLayoutControls.getLayoutComponentView(LayoutComponent.BUTTON_SWAP_SCREENS)?.view?.setOnTouchListener(SingleButtonInputHandler(frontendInputHandler, Input.SWAP_SCREENS, enableHapticFeedback, touchVibrator))
binding.viewLayoutControls.getLayoutComponentView(LayoutComponent.BUTTON_QUICK_SAVE)?.view?.setOnTouchListener(SingleButtonInputHandler(frontendInputHandler, Input.QUICK_SAVE, enableHapticFeedback, touchVibrator))
binding.viewLayoutControls.getLayoutComponentView(LayoutComponent.BUTTON_QUICK_LOAD)?.view?.setOnTouchListener(SingleButtonInputHandler(frontendInputHandler, Input.QUICK_LOAD, enableHapticFeedback, touchVibrator))

binding.viewLayoutControls.getLayoutComponentViews().forEach {
if (!it.component.isScreen()) {
Expand Down Expand Up @@ -407,6 +417,14 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
updateSoftInput()
}

private fun performQuickSave() {
delegate.performQuickSave()
}

private fun performQuickLoad() {
delegate.performQuickLoad()
}

private fun updateRendererScreenAreas() {
val (topScreen, bottomScreen) = if (screensSwapped) {
LayoutComponent.BOTTOM_SCREEN to LayoutComponent.TOP_SCREEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ abstract class EmulatorDelegate(protected val activity: EmulatorActivity) {
abstract fun getEmulatorConfiguration(): EmulatorConfiguration
abstract fun getPauseMenuOptions(): List<EmulatorActivity.PauseMenuOption>
abstract fun onPauseMenuOptionSelected(option: EmulatorActivity.PauseMenuOption)
abstract fun performQuickSave()
abstract fun performQuickLoad()
abstract fun getCrashContext(): Any
abstract fun dispose()
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class EmulatorViewModel @Inject constructor(

fun getRomLoader(rom: Rom): Single<Pair<Rom, Uri>> {
val fileRomProcessor = romFileProcessorFactory.getFileRomProcessorForDocument(rom.uri)
// ?.flatMap { WfcRomPatcher().patchRom(rom, it).andThen(Single.just(it)) }
return fileRomProcessor?.getRealRomUri(rom)?.map { rom to it } ?: Single.error(RomLoadException("Unsupported ROM file extension"))
}

Expand All @@ -182,6 +183,10 @@ class EmulatorViewModel @Inject constructor(
return saveStatesRepository.getRomSaveStates(rom)
}

fun getRomQuickSaveStateSlot(rom: Rom): SaveStateSlot {
return saveStatesRepository.getRomQuickSaveStateSlot(rom)
}

fun getRomSaveStateSlotUri(rom: Rom, saveState: SaveStateSlot): Uri {
return saveStatesRepository.getRomSaveStateUri(rom, saveState)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.magnum.melonds.ui.emulator.firmware

import android.os.Bundle
import android.widget.Toast
import io.reactivex.Completable
import io.reactivex.Single
import me.magnum.melonds.MelonEmulator
Expand Down Expand Up @@ -63,6 +64,18 @@ class FirmwareEmulatorDelegate(activity: EmulatorActivity) : EmulatorDelegate(ac
}
}

override fun performQuickSave() {
showSaveStatesNotSupportedToast()
}

override fun performQuickLoad() {
showSaveStatesNotSupportedToast()
}

private fun showSaveStatesNotSupportedToast() {
Toast.makeText(activity, R.string.save_states_not_supported, Toast.LENGTH_LONG).show()
}

override fun getCrashContext(): Any {
return FirmwareCrashContext(getEmulatorConfiguration(), activity.viewModel.getDsBiosDirectory()?.toString(), activity.viewModel.getDsiBiosDirectory()?.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ abstract class FrontendInputHandler : IInputListener {
Input.TOGGLE_SOFT_INPUT -> onSoftInputTogglePressed()
Input.RESET -> onResetPressed()
Input.SWAP_SCREENS -> onSwapScreens()
Input.QUICK_SAVE -> onQuickSave()
Input.QUICK_LOAD -> onQuickLoad()
}
}

Expand All @@ -25,4 +27,6 @@ abstract class FrontendInputHandler : IInputListener {
abstract fun onSoftInputTogglePressed()
abstract fun onResetPressed()
abstract fun onSwapScreens()
abstract fun onQuickSave()
abstract fun onQuickLoad()
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,25 +135,11 @@ class RomEmulatorDelegate(activity: EmulatorActivity, private val picasso: Picas
when (option) {
RomPauseMenuOptions.SETTINGS -> activity.openSettings()
RomPauseMenuOptions.SAVE_STATE -> pickSaveStateSlot {
val saveStateUri = activity.viewModel.getRomSaveStateSlotUri(loadedRom, it)
if (MelonEmulator.saveState(saveStateUri)) {
val screenshot = activity.takeScreenshot()
activity.viewModel.setRomSaveStateSlotScreenshot(loadedRom, it, screenshot)
} else {
Toast.makeText(activity, activity.getString(R.string.failed_save_state), Toast.LENGTH_SHORT).show()
}

saveState(it)
activity.resumeEmulation()
}
RomPauseMenuOptions.LOAD_STATE -> pickSaveStateSlot {
if (!it.exists) {
Toast.makeText(activity, activity.getString(R.string.cant_load_empty_slot), Toast.LENGTH_SHORT).show()
} else {
val saveStateUri = activity.viewModel.getRomSaveStateSlotUri(loadedRom, it)
if (!MelonEmulator.loadState(saveStateUri))
Toast.makeText(activity, activity.getString(R.string.failed_load_state), Toast.LENGTH_SHORT).show()
}

loadState(it)
activity.resumeEmulation()
}
RomPauseMenuOptions.CHEATS -> openCheatsActivity()
Expand All @@ -162,6 +148,24 @@ class RomEmulatorDelegate(activity: EmulatorActivity, private val picasso: Picas
}
}

override fun performQuickSave() {
MelonEmulator.pauseEmulation()
val quickSlot = activity.viewModel.getRomQuickSaveStateSlot(loadedRom)
if (saveState(quickSlot)) {
Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show()
}
MelonEmulator.resumeEmulation()
}

override fun performQuickLoad() {
MelonEmulator.pauseEmulation()
val quickSlot = activity.viewModel.getRomQuickSaveStateSlot(loadedRom)
if (loadState(quickSlot)) {
Toast.makeText(activity, R.string.loaded, Toast.LENGTH_SHORT).show()
}
MelonEmulator.resumeEmulation()
}

override fun getCrashContext(): Any {
val sramUri = try {
activity.viewModel.getRomSramFile(loadedRom)
Expand All @@ -175,6 +179,33 @@ class RomEmulatorDelegate(activity: EmulatorActivity, private val picasso: Picas
cheatsLoadDisposable?.dispose()
}

private fun saveState(slot: SaveStateSlot): Boolean {
val saveStateUri = activity.viewModel.getRomSaveStateSlotUri(loadedRom, slot)
return if (MelonEmulator.saveState(saveStateUri)) {
val screenshot = activity.takeScreenshot()
activity.viewModel.setRomSaveStateSlotScreenshot(loadedRom, slot, screenshot)
true
} else {
Toast.makeText(activity, activity.getString(R.string.failed_save_state), Toast.LENGTH_SHORT).show()
false
}
}

private fun loadState(slot: SaveStateSlot): Boolean {
return if (!slot.exists) {
Toast.makeText(activity, activity.getString(R.string.cant_load_empty_slot), Toast.LENGTH_SHORT).show()
false
} else {
val saveStateUri = activity.viewModel.getRomSaveStateSlotUri(loadedRom, slot)
if (!MelonEmulator.loadState(saveStateUri)) {
Toast.makeText(activity, activity.getString(R.string.failed_load_state), Toast.LENGTH_SHORT).show()
false
} else {
true
}
}
}

private fun loadRomCheats(rom: Rom): Maybe<List<Cheat>> {
return Maybe.create<List<Cheat>> { emitter ->
val romInfo = activity.viewModel.getRomInfo(rom)
Expand Down
Loading

0 comments on commit 3b8c582

Please sign in to comment.