From 61ba5e7bff807a889454016ac0ed730244998e87 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Sat, 15 Jul 2023 20:13:35 +0000
Subject: [PATCH] add physical controller support

---
 .../org/ryujinx/android/GameController.kt     | 15 +++-
 .../main/java/org/ryujinx/android/GameHost.kt | 11 ++-
 .../java/org/ryujinx/android/MainActivity.kt  | 20 ++++++
 .../android/PhysicalControllerManager.kt      | 69 +++++++++++++++++++
 .../android/viewmodels/QuickSettings.kt       |  2 +
 .../android/viewmodels/SettingsViewModel.kt   |  8 ++-
 .../org/ryujinx/android/views/SettingViews.kt | 31 ++++++++-
 7 files changed, 147 insertions(+), 9 deletions(-)
 create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt

diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt
index d3c5baa1b..7323876c6 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleCoroutineScope
 import androidx.lifecycle.flowWithLifecycle
@@ -45,6 +46,7 @@ typealias GamePad = RadialGamePad
 typealias GamePadConfig = RadialGamePadConfig
 
 class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) {
+    private var controllerView: View? = null
     var leftGamePad: GamePad
     var rightGamePad: GamePad
     var controllerId: Int = -1
@@ -65,7 +67,6 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
     @Composable
     fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit
     {
-
         AndroidView(
             modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)})
 
@@ -81,14 +82,22 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
         }
     }
 
-    private  fun Create(context: Context) : View
+    private fun Create(context: Context) : View
     {
         var inflator = LayoutInflater.from(context);
         var view = inflator.inflate(R.layout.game_layout, null)
         view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad);
         view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad);
 
-        return view as View
+        controllerView = view
+
+        return controllerView as View
+    }
+
+    fun setVisible(isVisible: Boolean){
+        controllerView?.apply {
+            this.isVisible = isVisible
+        }
     }
 
     fun connect(){
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt
index a79306c8f..563bd4f28 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt
@@ -132,7 +132,16 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
 
         _nativeRyujinx.inputInitialize(width, height)
 
-        controller.connect()
+        if(!settings.useVirtualController){
+            controller.setVisible(false)
+        }
+        else{
+            controller.connect()
+        }
+
+        mainViewModel.activity.physicalControllerManager.connect()
+
+        //
 
         _nativeRyujinx.graphicsRendererSetSize(
             surfaceHolder.surfaceFrame.width(),
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt
index 422b05fed..8458adf80 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt
@@ -1,5 +1,6 @@
 package org.ryujinx.android
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.content.pm.ActivityInfo
@@ -8,6 +9,8 @@ import android.media.AudioManager
 import android.os.Build
 import android.os.Bundle
 import android.os.Environment
+import android.view.KeyEvent
+import android.view.MotionEvent
 import android.view.WindowManager
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
@@ -29,6 +32,7 @@ import org.ryujinx.android.views.MainView
 
 
 class MainActivity : ComponentActivity() {
+    var physicalControllerManager: PhysicalControllerManager
     private var mainViewModel: MainViewModel? = null
     private var _isInit: Boolean = false
     var storageHelper: SimpleStorageHelper? = null
@@ -41,6 +45,7 @@ class MainActivity : ComponentActivity() {
     }
 
     init {
+        physicalControllerManager = PhysicalControllerManager(this)
         storageHelper = SimpleStorageHelper(this)
         StorageHelper = storageHelper
     }
@@ -79,6 +84,21 @@ class MainActivity : ComponentActivity() {
         }
     }
 
+    @SuppressLint("RestrictedApi")
+    override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+        event?.apply {
+            return physicalControllerManager.onKeyEvent(this)
+        }
+        return super.dispatchKeyEvent(event)
+    }
+
+    override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
+        ev?.apply {
+            physicalControllerManager.onMotionEvent(this)
+        }
+        return super.dispatchGenericMotionEvent(ev)
+    }
+
     private fun initialize() : Unit
     {
         if(_isInit)
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt
new file mode 100644
index 000000000..fe372bdcd
--- /dev/null
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt
@@ -0,0 +1,69 @@
+package org.ryujinx.android
+
+import android.view.KeyEvent
+import android.view.MotionEvent
+
+class PhysicalControllerManager(val activity: MainActivity) {
+    private var controllerId: Int = -1
+    private var ryujinxNative: RyujinxNative = RyujinxNative()
+
+    fun onKeyEvent(event: KeyEvent) : Boolean{
+        if(controllerId != -1) {
+            var id = GetGamePadButtonInputId(event.keyCode)
+
+            if(id != GamePadButtonInputId.None) {
+                when (event.action) {
+                    KeyEvent.ACTION_UP -> {
+                        ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
+                    }
+
+                    KeyEvent.ACTION_DOWN -> {
+                        ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId)
+                    }
+                }
+                return true;
+            }
+        }
+
+        return  false
+    }
+
+    fun onMotionEvent(ev: MotionEvent) {
+        if(controllerId != -1) {
+            if(ev.action == MotionEvent.ACTION_MOVE) {
+                var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X);
+                var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y);
+                var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z);
+                var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ);
+                ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
+                ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
+            }
+        }
+    }
+
+    fun connect(){
+        controllerId = ryujinxNative.inputConnectGamepad(0)
+    }
+
+    fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
+        return when (keycode) {
+            KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
+            KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
+            KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X
+            KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y
+            KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
+            KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
+            KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
+            KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger
+            KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick
+            KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick
+            KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp
+            KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown
+            KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft
+            KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight
+            KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus
+            KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus
+            else -> GamePadButtonInputId.None
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt
index 188e9ec6f..de1d6b320 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt
@@ -10,6 +10,7 @@ class QuickSettings(val activity: MainActivity) {
     var enableDocked: Boolean
     var enableVsync: Boolean
     var useNce: Boolean
+    var useVirtualController: Boolean
     var isHostMapped: Boolean
     var enableShaderCache: Boolean
     var enableTextureRecompression: Boolean
@@ -27,5 +28,6 @@ class QuickSettings(val activity: MainActivity) {
         enableShaderCache = sharedPref.getBoolean("enableShaderCache", true)
         enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
         resScale = sharedPref.getFloat("resScale", 1f)
+        useVirtualController = sharedPref.getBoolean("useVirtualController", true)
     }
 }
\ No newline at end of file
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt
index 095398921..73ed910ad 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt
@@ -26,7 +26,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
         ignoreMissingServices: MutableState<Boolean>,
         enableShaderCache: MutableState<Boolean>,
         enableTextureRecompression: MutableState<Boolean>,
-        resScale: MutableState<Float>
+        resScale: MutableState<Float>,
+        useVirtualController: MutableState<Boolean>
     )
     {
 
@@ -39,6 +40,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
         enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
         enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
         resScale.value = sharedPref.getFloat("resScale", 1f)
+        useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
     }
 
     fun save(
@@ -50,7 +52,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
         ignoreMissingServices: MutableState<Boolean>,
         enableShaderCache: MutableState<Boolean>,
         enableTextureRecompression: MutableState<Boolean>,
-        resScale: MutableState<Float>
+        resScale: MutableState<Float>,
+        useVirtualController: MutableState<Boolean>
     ){
         var editor = sharedPref.edit()
 
@@ -63,6 +66,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
         editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true)
         editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false)
         editor.putFloat("resScale", resScale?.value ?: 1f)
+        editor.putBoolean("useVirtualController", useVirtualController?.value ?: true)
 
         editor.apply()
     }
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
index 3546fe7b5..6ba85e1ad 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
@@ -92,6 +92,9 @@ class SettingViews {
             var resScale = remember {
                 mutableStateOf(1f)
             }
+            var useVirtualController = remember {
+                mutableStateOf(true)
+            }
 
             if (!loaded.value) {
                 settingsViewModel.initializeState(
@@ -100,7 +103,8 @@ class SettingViews {
                     enableVsync, enableDocked, enablePtc, ignoreMissingServices,
                     enableShaderCache,
                     enableTextureRecompression,
-                    resScale
+                    resScale,
+                    useVirtualController
                 )
                 loaded.value = true
             }
@@ -121,7 +125,8 @@ class SettingViews {
                                     ignoreMissingServices,
                                     enableShaderCache,
                                     enableTextureRecompression,
-                                    resScale
+                                    resScale,
+                                    useVirtualController
                                 )
                                 settingsViewModel.navController.popBackStack()
                             }) {
@@ -136,7 +141,8 @@ class SettingViews {
                             useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
                             enableShaderCache,
                             enableTextureRecompression,
-                            resScale
+                            resScale,
+                            useVirtualController
                         )
                     }
                     ExpandableView(onCardArrowClick = { }, title = "System") {
@@ -431,6 +437,25 @@ class SettingViews {
                             */
                         }
                     }
+                    ExpandableView(onCardArrowClick = { }, title = "Input") {
+                        Column(modifier = Modifier.fillMaxWidth()) {
+                            Row(
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .padding(8.dp),
+                                horizontalArrangement = Arrangement.SpaceBetween,
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                Text(
+                                    text = "Show virtual controller",
+                                    modifier = Modifier.align(Alignment.CenterVertically)
+                                )
+                                Switch(checked = useVirtualController.value, onCheckedChange = {
+                                    useVirtualController.value = !useVirtualController.value
+                                })
+                            }
+                        }
+                    }
                 }
             }
         }