Added quick menu select/start combo to open with controller

This commit is contained in:
izzy2lost
2026-01-12 02:26:44 -05:00
parent 6308ef50f4
commit 03757d65a1
5 changed files with 399 additions and 7 deletions
+2
View File
@@ -57,6 +57,8 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildToolsVersion = "36.1.0"
ndkVersion = "29.0.14206865"
}
dependencies {
@@ -13,6 +13,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.view.PixelCopy
import android.view.KeyEvent
import android.view.InputDevice
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.Surface
@@ -41,12 +42,17 @@ import org.libsdl.app.SDLActivity
* library specified by SDL_MAIN_LIBRARY (set to "super3" in the manifest).
*/
class Super3Activity : SDLActivity() {
private companion object {
private const val COMBO_WINDOW_MS = 120L
}
private val prefs by lazy { getSharedPreferences("super3_prefs", MODE_PRIVATE) }
private val mainHandler = Handler(Looper.getMainLooper())
private var overlayView: View? = null
private var overlayControlsEnabled: Boolean = true
private var gyroSteeringEnabled: Boolean = false
private var menuPaused = false
private var quickMenuOpen = false
private var saveDialogOpen = false
private var exitDialogOpen = false
private var userPaused = false
@@ -55,6 +61,20 @@ class Super3Activity : SDLActivity() {
private var gameName: String = ""
private var userDataRoot: File? = null
private var comboStartDown = false
private var comboSelectDown = false
private var comboTriggered = false
private var comboStartDownTime = 0L
private var comboSelectDownTime = 0L
private var comboStartDispatched = false
private var comboSelectDispatched = false
private var pendingStartDownEvent: KeyEvent? = null
private var pendingSelectDownEvent: KeyEvent? = null
private var pendingStartUpEvent: KeyEvent? = null
private var pendingSelectUpEvent: KeyEvent? = null
private var startDispatchToken = 0
private var selectDispatchToken = 0
private var gyroSensorManager: SensorManager? = null
private var gyroSensor: Sensor? = null
private var gyroListener: SensorEventListener? = null
@@ -116,12 +136,6 @@ class Super3Activity : SDLActivity() {
),
)
if (!overlayControlsEnabled) {
overlay.findViewById<View>(R.id.overlay_controls_root)?.visibility = View.GONE
overlay.visibility = View.GONE
return
}
overlay.findViewById<View>(R.id.overlay_save_state)?.setOnClickListener {
showSaveStateDialog()
}
@@ -393,6 +407,8 @@ class Super3Activity : SDLActivity() {
overlay.findViewById<View>(R.id.overlay_fight_stick)?.setOnTouchListener(null)
}
applyOverlayControlsEnabled(overlayControlsEnabled, persist = false)
if (isRacing || isMagTruck) {
val gasId = if (isMagTruck) 1130 else 1103
val brakeId = if (isMagTruck) 1131 else 1104
@@ -627,11 +643,15 @@ class Super3Activity : SDLActivity() {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
if (quickMenuOpen || saveDialogOpen || exitDialogOpen) {
return super.dispatchKeyEvent(event)
}
if (event.action == KeyEvent.ACTION_UP) {
handleBackPress()
}
return true
}
if (handleStartSelectCombo(event)) return true
return super.dispatchKeyEvent(event)
}
@@ -701,12 +721,265 @@ class Super3Activity : SDLActivity() {
}
private fun updatePauseState() {
val shouldPause = userPaused || exitDialogOpen || saveDialogOpen || capturingThumbnail
val shouldPause = userPaused || exitDialogOpen || saveDialogOpen || quickMenuOpen || capturingThumbnail
if (shouldPause == menuPaused) return
menuPaused = shouldPause
nativeSetMenuPaused(shouldPause)
}
private fun showQuickOptionsMenu() {
if (quickMenuOpen || exitDialogOpen || saveDialogOpen || capturingThumbnail) return
quickMenuOpen = true
updatePauseState()
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_quick_menu, null)
val btnPauseResume = dialogView.findViewById<MaterialButton>(R.id.btn_pause_resume)
val btnSaveStates = dialogView.findViewById<MaterialButton>(R.id.btn_save_states)
val btnTouchControls = dialogView.findViewById<MaterialButton>(R.id.btn_touch_controls)
val btnGyroSteering = dialogView.findViewById<MaterialButton>(R.id.btn_gyro_steering)
val btnExitGame = dialogView.findViewById<MaterialButton>(R.id.btn_exit_game)
// Set initial text states
btnPauseResume.text = getString(if (userPaused) R.string.quick_menu_resume else R.string.quick_menu_pause)
btnTouchControls.text = getString(if (overlayControlsEnabled) R.string.quick_menu_hide_touch_controls else R.string.quick_menu_show_touch_controls)
btnGyroSteering.text = getString(if (gyroSteeringEnabled) R.string.quick_menu_disable_gyro else R.string.quick_menu_enable_gyro)
val dialog = MaterialAlertDialogBuilder(
this,
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog,
)
.setTitle(R.string.quick_menu_title)
.setView(dialogView)
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setOnDismissListener {
quickMenuOpen = false
updatePauseState()
}
.create()
btnPauseResume.setOnClickListener {
userPaused = !userPaused
updatePauseState()
dialog.dismiss()
}
btnSaveStates.setOnClickListener {
dialog.dismiss()
mainHandler.post { showSaveStateDialog() }
}
btnTouchControls.setOnClickListener {
applyOverlayControlsEnabled(!overlayControlsEnabled, persist = true)
dialog.dismiss()
}
btnGyroSteering.setOnClickListener {
applyGyroSteeringEnabled(!gyroSteeringEnabled, persist = true)
dialog.dismiss()
}
btnExitGame.setOnClickListener {
dialog.dismiss()
finish()
}
dialog.show()
}
private fun applyOverlayControlsEnabled(enabled: Boolean, persist: Boolean) {
overlayControlsEnabled = enabled
if (persist) {
prefs.edit().putBoolean("overlay_controls_enabled", enabled).apply()
}
val overlay = overlayView
overlay?.findViewById<View>(R.id.overlay_controls_root)?.visibility = if (enabled) View.VISIBLE else View.GONE
overlay?.visibility = if (enabled) View.VISIBLE else View.GONE
}
private fun applyGyroSteeringEnabled(enabled: Boolean, persist: Boolean) {
gyroSteeringEnabled = enabled
if (persist) {
prefs.edit().putBoolean("gyro_steering_enabled", enabled).apply()
}
if (enabled) {
startGyroSteering()
} else {
stopGyroSteering()
}
}
private fun handleStartSelectCombo(event: KeyEvent): Boolean {
if (!event.isFromSource(InputDevice.SOURCE_GAMEPAD) && !event.isFromSource(InputDevice.SOURCE_JOYSTICK)) {
return false
}
val isStart = event.keyCode == KeyEvent.KEYCODE_BUTTON_START
val isSelect = event.keyCode == KeyEvent.KEYCODE_BUTTON_SELECT
if (!isStart && !isSelect) return false
if (quickMenuOpen || saveDialogOpen || exitDialogOpen || capturingThumbnail) {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (isStart) comboStartDown = true else comboSelectDown = true
}
KeyEvent.ACTION_UP -> {
if (isStart) comboStartDown = false else comboSelectDown = false
if (!comboStartDown && !comboSelectDown) comboTriggered = false
}
}
cancelPendingComboDispatches()
return true
}
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (event.repeatCount > 0) {
if (comboTriggered) return true
if ((isStart && comboStartDispatched) || (isSelect && comboSelectDispatched)) {
deliverToSdl(event)
}
return true
}
if (isStart) {
comboStartDown = true
comboStartDownTime = event.eventTime
if (!comboStartDispatched && pendingStartDownEvent == null) {
pendingStartDownEvent = KeyEvent(event)
startDispatchToken += 1
val token = startDispatchToken
mainHandler.postDelayed({ flushPendingStart(token) }, COMBO_WINDOW_MS)
}
} else {
comboSelectDown = true
comboSelectDownTime = event.eventTime
if (!comboSelectDispatched && pendingSelectDownEvent == null) {
pendingSelectDownEvent = KeyEvent(event)
selectDispatchToken += 1
val token = selectDispatchToken
mainHandler.postDelayed({ flushPendingSelect(token) }, COMBO_WINDOW_MS)
}
}
if (!comboTriggered && comboStartDown && comboSelectDown) {
val delta = kotlin.math.abs(comboStartDownTime - comboSelectDownTime)
if (delta <= COMBO_WINDOW_MS) {
comboTriggered = true
cancelPendingComboDispatches()
showQuickOptionsMenu()
}
}
return true
}
KeyEvent.ACTION_UP -> {
if (isStart) {
comboStartDown = false
comboStartDownTime = 0L
} else {
comboSelectDown = false
comboSelectDownTime = 0L
}
if (comboTriggered) {
if (!comboStartDown && !comboSelectDown) {
comboTriggered = false
}
return true
}
if (isStart) {
val down = pendingStartDownEvent
if (!comboStartDispatched && down != null) {
pendingStartDownEvent = null
deliverToSdl(down)
deliverToSdl(KeyEvent(event))
comboStartDispatched = false
pendingStartUpEvent = null
return true
}
if (comboStartDispatched) {
deliverToSdl(event)
comboStartDispatched = false
return true
}
pendingStartUpEvent = KeyEvent(event)
return true
} else {
val down = pendingSelectDownEvent
if (!comboSelectDispatched && down != null) {
pendingSelectDownEvent = null
deliverToSdl(down)
deliverToSdl(KeyEvent(event))
comboSelectDispatched = false
pendingSelectUpEvent = null
return true
}
if (comboSelectDispatched) {
deliverToSdl(event)
comboSelectDispatched = false
return true
}
pendingSelectUpEvent = KeyEvent(event)
return true
}
}
else -> return true
}
}
private fun cancelPendingComboDispatches() {
startDispatchToken += 1
selectDispatchToken += 1
pendingStartDownEvent = null
pendingSelectDownEvent = null
pendingStartUpEvent = null
pendingSelectUpEvent = null
comboStartDispatched = false
comboSelectDispatched = false
}
private fun flushPendingStart(token: Int) {
if (token != startDispatchToken) return
if (comboTriggered || quickMenuOpen || saveDialogOpen || exitDialogOpen || capturingThumbnail) {
pendingStartDownEvent = null
pendingStartUpEvent = null
comboStartDispatched = false
return
}
val down = pendingStartDownEvent ?: return
pendingStartDownEvent = null
deliverToSdl(down)
comboStartDispatched = true
pendingStartUpEvent?.let { up ->
pendingStartUpEvent = null
deliverToSdl(up)
comboStartDispatched = false
}
}
private fun flushPendingSelect(token: Int) {
if (token != selectDispatchToken) return
if (comboTriggered || quickMenuOpen || saveDialogOpen || exitDialogOpen || capturingThumbnail) {
pendingSelectDownEvent = null
pendingSelectUpEvent = null
comboSelectDispatched = false
return
}
val down = pendingSelectDownEvent ?: return
pendingSelectDownEvent = null
deliverToSdl(down)
comboSelectDispatched = true
pendingSelectUpEvent?.let { up ->
pendingSelectUpEvent = null
deliverToSdl(up)
comboSelectDispatched = false
}
}
private fun deliverToSdl(event: KeyEvent) {
super.dispatchKeyEvent(event)
}
private fun showSaveStateDialog() {
val view = layoutInflater.inflate(R.layout.dialog_saves, null, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_save_slots)
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,600L200,600L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM420,680L364,622L466,520L120,520L120,440L466,440L364,338L420,280L620,480L420,680Z"/>
</vector>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<!-- Pause/Resume -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_pause_resume"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_pause"
android:textAlignment="viewStart"
app:icon="@drawable/play_pause_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
<!-- Save States -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_save_states"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_save_states"
android:textAlignment="viewStart"
app:icon="@drawable/save_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
<!-- Touch Controls -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_touch_controls"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_show_touch_controls"
android:textAlignment="viewStart"
app:icon="@drawable/stadia_controller_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
<!-- Gyro Steering -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_gyro_steering"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_enable_gyro"
android:textAlignment="viewStart"
app:icon="@drawable/wheel"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
<!-- Exit Game -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_exit_game"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_exit_game"
android:textAlignment="viewStart"
app:icon="@drawable/exit_to_app_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorError" />
</LinearLayout>
@@ -15,6 +15,17 @@
<string name="exit_game_message">Are you sure you want to exit the game?</string>
<string name="exit_game_confirm">Exit</string>
<string name="exit_game_cancel">Cancel</string>
<string name="quick_menu_title">Quick options</string>
<string name="quick_menu_pause">Pause</string>
<string name="quick_menu_resume">Resume</string>
<string name="quick_menu_save_states">Save states…</string>
<string name="quick_menu_quick_save_format">Quick save (slot %1$d)</string>
<string name="quick_menu_quick_load_format">Quick load (slot %1$d)</string>
<string name="quick_menu_show_touch_controls">Show touch controls</string>
<string name="quick_menu_hide_touch_controls">Hide touch controls</string>
<string name="quick_menu_enable_gyro">Enable gyro steering</string>
<string name="quick_menu_disable_gyro">Disable gyro steering</string>
<string name="quick_menu_exit_game">Exit game</string>
<string name="setup_title">Welcome to SUPER3</string>
<string name="setup_subtitle">Quick setup for your folders.</string>
<string name="setup_games_title">Choose your games folder</string>