android: add quicksave hotkeys (#181)

This commit is contained in:
Felix Nüsse 2024-07-14 00:54:19 +02:00 committed by GitHub
parent ed3d5a9f7f
commit 45f52709a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 96 additions and 18 deletions

View file

@ -527,12 +527,28 @@ object NativeLibrary {
external fun removeAmiibo() external fun removeAmiibo()
const val SAVESTATE_SLOT_COUNT = 10 const val SAVESTATE_SLOT_COUNT = 11
const val QUICKSAVE_SLOT = 0
external fun getSavestateInfo(): Array<SaveStateInfo>? external fun getSavestateInfo(): Array<SaveStateInfo>?
external fun saveState(slot: Int) external fun saveState(slot: Int)
fun loadStateIfAvailable(slot: Int): Boolean {
var available = false
getSavestateInfo()?.forEach {
if (it.slot == slot){
available = true
return@forEach
}
}
if (available) {
loadState(slot)
return true
}
return false
}
external fun loadState(slot: Int) external fun loadState(slot: Int)
/** /**

View file

@ -66,7 +66,7 @@ class EmulationActivity : AppCompatActivity() {
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings) screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil) hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this)
setContentView(binding.root) setContentView(binding.root)
val navHostFragment = val navHostFragment =

View file

@ -8,5 +8,7 @@ enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001), SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002), CYCLE_LAYOUT(10002),
CLOSE_GAME(10003), CLOSE_GAME(10003),
PAUSE_OR_RESUME(10004); PAUSE_OR_RESUME(10004),
QUICKSAVE(10005),
QUICKLOAD(10006);
} }

View file

@ -4,10 +4,14 @@
package org.citra.citra_emu.features.hotkeys package org.citra.citra_emu.features.hotkeys
import android.content.Context
import android.widget.Toast
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.display.ScreenAdjustmentUtil
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) { class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, private val context: Context) {
val hotkeyButtons = Hotkey.entries.map { it.button } val hotkeyButtons = Hotkey.entries.map { it.button }
@ -18,6 +22,23 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
Hotkey.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(context,
context.getString(R.string.quicksave_saving),
Toast.LENGTH_SHORT).show()
}
Hotkey.QUICKLOAD.button -> {
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
val stringRes = if(wasLoaded) {
R.string.quickload_loading
} else {
R.string.quickload_not_found
}
Toast.makeText(context,
context.getString(stringRes),
Toast.LENGTH_SHORT).show()
}
else -> {} else -> {}
} }
return true return true

View file

@ -136,6 +136,8 @@ class Settings {
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game" const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game" const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
const val HOTKEY_QUICKSAVE = "hotkey_quickload"
const val HOTKEY_QUICKlOAD = "hotkey_quickpause"
val buttonKeys = listOf( val buttonKeys = listOf(
KEY_BUTTON_A, KEY_BUTTON_A,
@ -187,13 +189,17 @@ class Settings {
HOTKEY_SCREEN_SWAP, HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT, HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME, HOTKEY_CLOSE_GAME,
HOTKEY_PAUSE_OR_RESUME HOTKEY_PAUSE_OR_RESUME,
HOTKEY_QUICKSAVE,
HOTKEY_QUICKlOAD
) )
val hotkeyTitles = listOf( val hotkeyTitles = listOf(
R.string.emulation_swap_screens, R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts, R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game, R.string.emulation_close_game,
R.string.emulation_toggle_pause R.string.emulation_toggle_pause,
R.string.emulation_quicksave,
R.string.emulation_quickload,
) )
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"

View file

@ -133,6 +133,8 @@ class InputBindingSetting(
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button
Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button
else -> -1 else -> -1
} }

View file

@ -481,12 +481,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener { popupMenu.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_emulation_save_state -> { R.id.menu_emulation_save_state -> {
showSaveStateSubmenu() showStateSubmenu(true)
true true
} }
R.id.menu_emulation_load_state -> { R.id.menu_emulation_load_state -> {
showLoadStateSubmenu() showStateSubmenu(false)
true true
} }
@ -497,7 +497,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show() popupMenu.show()
} }
private fun showSaveStateSubmenu() { private fun showStateSubmenu(isSaving: Boolean) {
val savestates = NativeLibrary.getSavestateInfo() val savestates = NativeLibrary.getSavestateInfo()
val popupMenu = PopupMenu( val popupMenu = PopupMenu(
@ -507,19 +508,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menu.apply { popupMenu.menu.apply {
for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) { for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) {
val slot = i + 1 val slot = i
val text = getString(R.string.emulation_empty_state_slot, slot) var enableClick = isSaving
add(text).setEnabled(true).setOnMenuItemClickListener { val text = if (slot == NativeLibrary.QUICKSAVE_SLOT) {
displaySavestateWarning() enableClick = false
getString(R.string.emulation_quicksave_slot)
} else {
getString(R.string.emulation_empty_state_slot, slot)
}
add(text).setEnabled(enableClick).setOnMenuItemClickListener {
if(isSaving) {
NativeLibrary.saveState(slot) NativeLibrary.saveState(slot)
} else {
NativeLibrary.loadState(slot)
binding.drawerLayout.close()
Toast.makeText(context,
getString(R.string.quickload_loading),
Toast.LENGTH_SHORT).show()
}
true true
} }
} }
} }
savestates?.forEach { savestates?.forEach {
val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time) var enableClick = true
popupMenu.menu.getItem(it.slot - 1).setTitle(text) val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) {
// do not allow saving in quicksave slot
enableClick = !isSaving
getString(R.string.emulation_occupied_quicksave_slot, it.time)
} else{
getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
}
popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick)
} }
popupMenu.show() popupMenu.show()

View file

@ -692,4 +692,13 @@
<string name="delay_render_thread">Delay game render thread</string> <string name="delay_render_thread">Delay game render thread</string>
<string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games.</string> <string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games.</string>
<!-- Quickload&Save-->
<string name="emulation_quicksave_slot">Quicksave</string>
<string name="emulation_quicksave">Quicksave</string>
<string name="emulation_quickload">Quickload</string>
<string name="emulation_occupied_quicksave_slot">Quicksave - %1$tF %1$tR</string>
<string name="quicksave_saving">Saving…</string>
<string name="quickload_loading">Loading…</string>
<string name="quickload_not_found">No Quicksave available.</string>
</resources> </resources>

View file

@ -90,7 +90,7 @@ static bool ValidateSaveState(const CSTHeader& header, SaveStateInfo& info, u64
std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id) { std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id) {
std::vector<SaveStateInfo> result; std::vector<SaveStateInfo> result;
result.reserve(SaveStateSlotCount); result.reserve(SaveStateSlotCount);
for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) { for (u32 slot = 0; slot <= SaveStateSlotCount; ++slot) {
const auto path = GetSaveStatePath(program_id, movie_id, slot); const auto path = GetSaveStatePath(program_id, movie_id, slot);
if (!FileUtil::Exists(path)) { if (!FileUtil::Exists(path)) {
continue; continue;

View file

@ -20,7 +20,7 @@ struct SaveStateInfo {
std::string build_name; std::string build_name;
}; };
constexpr u32 SaveStateSlotCount = 10; // Maximum count of savestate slots constexpr u32 SaveStateSlotCount = 11; // Maximum count of savestate slots
std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id); std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id);