Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/main/feature/retro/Gen1EmbedLaunch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ object Gen1EmbedLaunch {
Gen1EngineActivity.EXTRA_ARTWORK_PATH,
shortcut.getExtra("customCoverArtPath"),
)
putExtra(
Gen1EngineActivity.EXTRA_ENGINE_VARS,
Gen1EngineSettings.resolve(context, shortcut),
)
}
}
}
133 changes: 133 additions & 0 deletions app/src/main/feature/retro/Gen1EngineActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
Expand All @@ -45,6 +49,7 @@ class Gen1EngineActivity :
private lateinit var bridge: Gen1EngineBridge

private var touchControls = true
private var engineVarsApplied = false

private var persistShortcut: Shortcut? = null

Expand Down Expand Up @@ -163,6 +168,12 @@ class Gen1EngineActivity :

override fun dispatchKeyEvent(event: android.view.KeyEvent): Boolean {
val keyCode = event.keyCode
if (keyCode == android.view.KeyEvent.KEYCODE_BUTTON_MODE && isGamepadSource(event)) {
if (event.action == android.view.KeyEvent.ACTION_UP) {
if (menu.visible) menu.close() else openMenu()
}
return true
}
if (keyCode == android.view.KeyEvent.KEYCODE_BACK) {
if (event.action == android.view.KeyEvent.ACTION_UP) {
if (menu.visible) {
Expand All @@ -180,6 +191,24 @@ class Gen1EngineActivity :
return super.dispatchKeyEvent(event)
}

override fun dispatchGenericMotionEvent(event: android.view.MotionEvent): Boolean {
val joystick =
event.source and android.view.InputDevice.SOURCE_JOYSTICK ==
android.view.InputDevice.SOURCE_JOYSTICK
if (menu.visible && joystick) {
val hatX = event.getAxisValue(android.view.MotionEvent.AXIS_HAT_X)
val hatY = event.getAxisValue(android.view.MotionEvent.AXIS_HAT_Y)
val x =
if (kotlin.math.abs(hatX) > 0.5f) hatX else event.getAxisValue(android.view.MotionEvent.AXIS_X)
val y =
if (kotlin.math.abs(hatY) > 0.5f) hatY else event.getAxisValue(android.view.MotionEvent.AXIS_Y)
menu.handleAxis(x, y)
return true
}
if (menu.visible) return true
return super.dispatchGenericMotionEvent(event)
}

private fun buildTabs(): List<RetroTabSpec> =
listOf(
RetroTabSpec(null, RetroDrawerIcons.Play, getString(R.string.retro_tab_menu)),
Expand Down Expand Up @@ -225,8 +254,99 @@ class Gen1EngineActivity :
else -> RetroPane.SYSTEM
}

private fun isGamepadSource(event: android.view.KeyEvent): Boolean {
val source = event.device?.sources ?: return false
return source and android.view.InputDevice.SOURCE_GAMEPAD == android.view.InputDevice.SOURCE_GAMEPAD ||
source and android.view.InputDevice.SOURCE_JOYSTICK == android.view.InputDevice.SOURCE_JOYSTICK
}

private fun stadiumRomEntry(row: Gen1EngineBridge.Row): RetroMenuEntry {
val building = Gen1StadiumRom.isBuilding(row.value) || Gen1StadiumRom.hasStagedPick(this)
val installed = Gen1StadiumRom.isInstalled(this)
return RetroMenuEntry.Action(
label = row.label,
icon = RetroDrawerIcons.EditLayout,
danger = installed && !building,
subtitle =
when {
building -> getString(R.string.retro_stadium_building)
installed -> getString(R.string.retro_stadium_ready)
else -> getString(R.string.retro_stadium_import)
},
) {
when {
building -> Unit
installed -> promptStadiumDelete()
else -> pickStadiumRom()
}
}
}

private fun pickStadiumRom() {
menu.close()
val intent =
android.content.Intent(android.content.Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(android.content.Intent.CATEGORY_OPENABLE)
type = "*/*"
}
runCatching { startActivityForResult(intent, REQUEST_STADIUM_ROM) }
.onFailure { toast(getString(R.string.retro_stadium_no_picker)) }
}

private fun promptStadiumDelete() {
menu.confirmPrompt =
RetroConfirmPrompt(
title = getString(R.string.retro_stadium_row),
message = getString(R.string.retro_stadium_delete_body),
confirmLabel = getString(R.string.retro_stadium_delete),
dismissLabel = getString(R.string.retro_stadium_keep),
onConfirm = {
menu.confirmPrompt = null
val gone = Gen1StadiumRom.delete(this)
toast(
getString(
if (gone) {
R.string.retro_stadium_deleted
} else {
R.string.retro_stadium_delete_failed
},
),
)
menu.rebuild()
pollFaster()
},
onDismiss = { menu.confirmPrompt = null },
)
}

override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: android.content.Intent?,
) {
if (requestCode != REQUEST_STADIUM_ROM) {
super.onActivityResult(requestCode, resultCode, data)
return
}
val uri = data?.data
if (resultCode != RESULT_OK || uri == null) return
toast(getString(R.string.retro_stadium_importing))
lifecycleScope.launch(Dispatchers.IO) {
val result = Gen1StadiumRom.stage(this@Gen1EngineActivity, uri)
withContext(Dispatchers.Main) {
result
.onSuccess {
toast(getString(R.string.retro_stadium_imported))
pollFaster()
}
.onFailure { toast(it.message ?: getString(R.string.retro_stadium_import_failed)) }
}
}
}

private fun rowEntry(row: Gen1EngineBridge.Row): RetroMenuEntry =
when {
row.id == Gen1StadiumRom.ROW_ID -> stadiumRomEntry(row)
row.values.isNotEmpty() ->
RetroMenuEntry.Choice(row.label, row.values, row.selectedIndex) { index ->
bridge.setRow(row.id, index)
Expand Down Expand Up @@ -631,6 +751,16 @@ class Gen1EngineActivity :
private fun onEngineState(state: Gen1EngineBridge.State, menuChanged: Boolean) {
importState = state.import
if (loadingVisible && state.booted) loadingVisible = false
if (state.booted && state.rows.isNotEmpty()) {
if (!engineVarsApplied) {
engineVarsApplied = true
@Suppress("UNCHECKED_CAST")
val wanted =
intent.getSerializableExtra(EXTRA_ENGINE_VARS) as? HashMap<String, String>
if (wanted != null) Gen1EngineSettings.applyTo(bridge, wanted)
}
Gen1EngineSettings.cache(this, state.rows)
}
if (menuChanged && menu.visible) menu.rebuild()
}

Expand Down Expand Up @@ -852,10 +982,13 @@ class Gen1EngineActivity :
companion object {
private const val TAG = "WnGen1Engine"

private const val REQUEST_STADIUM_ROM = 0x5D01

const val EXTRA_ROM_PATH = "wn.engine.rom"
const val EXTRA_VERSION = "wn.engine.version"
const val EXTRA_GAME_NAME = "wn.engine.game_name"
const val EXTRA_SHORTCUT_PATH = "wn.engine.shortcut"
const val EXTRA_ENGINE_VARS = "wn.engine.vars"
const val EXTRA_ARTWORK_PATH = "wn.engine.artwork"

private const val DPAD_DEADZONE = 0.35f
Expand Down
111 changes: 111 additions & 0 deletions app/src/main/feature/retro/Gen1EngineSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.winlator.cmod.feature.retro

import android.content.Context
import androidx.preference.PreferenceManager
import com.winlator.cmod.runtime.container.Shortcut
import org.json.JSONArray
import org.json.JSONObject

object Gen1EngineSettings {
private const val CACHE_KEY = "gen1_engine_rows"

data class CachedRow(
val id: String,
val label: String,
val values: List<String>,
val pane: String,
)

fun cache(
context: Context,
rows: List<Gen1EngineBridge.Row>,
) {
val usable = rows.filter { it.values.size > 1 && it.id != Gen1StadiumRom.ROW_ID }
if (usable.isEmpty()) return
val array = JSONArray()
usable.forEach { row ->
array.put(
JSONObject()
.put("id", row.id)
.put("label", row.label)
.put("pane", paneOf(row.id))
.put("values", JSONArray(row.values)),
)
}
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString(CACHE_KEY, array.toString()).apply()
}

fun cached(context: Context): List<CachedRow> =
runCatching {
val raw =
PreferenceManager.getDefaultSharedPreferences(context)
.getString(CACHE_KEY, null) ?: return emptyList()
val array = JSONArray(raw)
(0 until array.length()).mapNotNull { i ->
val o = array.optJSONObject(i) ?: return@mapNotNull null
val values = o.optJSONArray("values") ?: return@mapNotNull null
val list = (0 until values.length()).map { values.optString(it) }
if (list.size < 2) return@mapNotNull null
CachedRow(
id = o.optString("id"),
label = o.optString("label"),
values = list,
pane = o.optString("pane", PANE_SYSTEM),
)
}
}.getOrDefault(emptyList())

fun selection(
shortcut: Shortcut,
row: CachedRow,
): String? =
shortcut.getExtra(RetroShortcuts.VAR_PREFIX + row.id)
.takeIf { it.isNotEmpty() && it in row.values }

fun resolve(
context: Context,
shortcut: Shortcut,
): HashMap<String, String> {
val out = HashMap<String, String>()
cached(context).forEach { row ->
selection(shortcut, row)?.let { out[row.id] = it }
}
return out
}

fun applyTo(
bridge: Gen1EngineBridge,
wanted: Map<String, String>,
) {
if (wanted.isEmpty()) return
bridge.state.rows.forEach { row ->
val target = wanted[row.id] ?: return@forEach
if (row.values.isEmpty()) return@forEach
val index = row.values.indexOf(target)
if (index >= 0 && index != row.selectedIndex) bridge.setRow(row.id, index)
}
}

const val PANE_DISPLAY = "display"
const val PANE_SOUND = "sound"
const val PANE_PERFORMANCE = "performance"
const val PANE_CONTROLS = "controls"
const val PANE_SYSTEM = "system"

private val SOUND_ROWS = setOf("musicVol", "sfxVol", "pikaVol", "musicFilter")
private val DISPLAY_ROWS =
setOf("colors", "tilt", "gbcfx", "zoom", "voidFill", "videoMode", "animations")
private val PERFORMANCE_ROWS = setOf("fpsCap", "speed")
private val CONTROL_ROWS = setOf("controls")

private fun paneOf(id: String): String =
when {
Gen1EngineBridge.isModRow(id) -> PANE_DISPLAY
id in SOUND_ROWS -> PANE_SOUND
id in DISPLAY_ROWS -> PANE_DISPLAY
id in PERFORMANCE_ROWS -> PANE_PERFORMANCE
id in CONTROL_ROWS -> PANE_CONTROLS
else -> PANE_SYSTEM
}
}
Loading
Loading