diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8079524b..08f0cde1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,7 @@
+
+
{
+ mGLSurfaceView?.enableControllerEvents()
+ true
+ }
else -> false
}
}
diff --git a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt
index b077b35f..cb0dcb49 100644
--- a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt
+++ b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt
@@ -7,6 +7,9 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.hardware.lights.Light
+import android.hardware.lights.LightState
+import android.hardware.lights.LightsRequest
import android.net.Uri
import android.os.Build
import android.os.Environment
@@ -16,6 +19,7 @@ import android.os.VibratorManager
import android.provider.DocumentsContract
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.util.Log
+import android.view.InputDevice
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -547,6 +551,7 @@ object GeodeUtils {
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
const val CAPABILITY_INTERNAL_CALLBACKS = "internal_callbacks_v1"
+ const val CAPABILITY_CONTROLLER_DATA = "controller_data"
private var capabilityListener: WeakReference = WeakReference(null)
@@ -576,6 +581,188 @@ object GeodeUtils {
.launchUrl(activity, url.toUri())
}
+ /**
+ * Determines if any gamepad or joystick input devices are connected.
+ * This is useful during start, but tracking inputDeviceAdded events is better during runtime.
+ */
+ @JvmStatic
+ fun controllersConnected(): Int {
+ val deviceIds = InputDevice.getDeviceIds()
+ return deviceIds.count {
+ InputDevice.getDevice(it)?.run {
+ sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
+ } ?: false
+ }
+ }
+
+ @JvmStatic
+ fun getConnectedDevices(): IntArray = InputDevice.getDeviceIds()
+
+ @JvmStatic
+ fun getDevice(deviceId: Int): InputDevice? = InputDevice.getDevice(deviceId)
+
+ @JvmStatic
+ fun getDeviceBatteryCapacity(deviceId: Int): Float {
+ val device = InputDevice.getDevice(deviceId) ?: return 0.0f
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ device.batteryState.capacity
+ } else {
+ 0.0f
+ }
+ }
+
+ @JvmStatic
+ fun deviceHasBattery(deviceId: Int): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return false
+ }
+
+ val device = InputDevice.getDevice(deviceId) ?: return false
+ return device.batteryState.isPresent
+ }
+
+ @JvmStatic
+ fun getDeviceBatteryStatus(deviceId: Int): Int {
+ val device = InputDevice.getDevice(deviceId) ?: return 0
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ device.batteryState.status
+ } else {
+ 0
+ }
+ }
+
+ @JvmStatic
+ fun getDeviceLightsCount(deviceId: Int): Int {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return 0
+ }
+
+ val device = InputDevice.getDevice(deviceId) ?: return 0
+ val lightsManager = device.lightsManager
+
+ return lightsManager.lights.size
+ }
+
+ // this is based on paddleboat's api
+ @JvmStatic
+ fun getLightType(deviceId: Int): Int {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return 0
+ }
+
+ val device = InputDevice.getDevice(deviceId) ?: return 0
+ val lightsManager = device.lightsManager
+
+ var flags = 0
+ lightsManager.lights.forEach { light ->
+ if (light.type == Light.LIGHT_TYPE_PLAYER_ID) {
+ flags = flags or 1
+ } else if (light.hasRgbControl()) {
+ flags = flags or 2
+ }
+ }
+
+ return flags
+ }
+
+ /**
+ * Sets the color for a device.
+ * @return false if the given device does not exist or setting lights was otherwise unsuccessful
+ */
+ @JvmStatic
+ fun setDeviceLightColor(deviceId: Int, color: Int, type: Int): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return false
+ }
+
+ val device = InputDevice.getDevice(deviceId) ?: return false
+ val lightsManager = device.lightsManager
+
+ val lights = lightsManager.lights
+ if (lights.isEmpty()) {
+ return false
+ }
+
+ val actedLights = lights.count { light ->
+ val state = LightState.Builder()
+
+ if (light.type == Light.LIGHT_TYPE_PLAYER_ID && (type and 1 == 1)) {
+ state.setPlayerId(color)
+ } else if (light.hasRgbControl() && (type and 2 == 2)) {
+ state.setColor(color)
+ } else {
+ return@count false
+ }
+
+ val request = LightsRequest.Builder()
+ .addLight(light, state.build())
+ .build()
+
+ lightsManager.openSession()
+ .requestLights(request)
+
+ true
+ }
+
+ return actedLights > 0
+ }
+
+ @JvmStatic
+ fun getDeviceHapticsCount(deviceId: Int): Int {
+ val device = InputDevice.getDevice(deviceId) ?: return 0
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return if (device.vibrator.hasVibrator()) 1 else 0
+ }
+
+ val vibratorManager = device.vibratorManager
+ val vibratorIds = vibratorManager.vibratorIds
+ return vibratorIds.size
+ }
+
+ private fun performVibration(vibrator: Vibrator?, durationMs: Long, intensity: Int) {
+ vibrator?.apply {
+ if (intensity == 0) {
+ cancel()
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val vibrationEffect = VibrationEffect.createOneShot(durationMs, intensity)
+ vibrate(vibrationEffect)
+ } else {
+ vibrate(durationMs)
+ }
+ }
+ }
+ }
+
+ /**
+ * Vibrates a given device. Set motorIdx == -1 to vibrate all available motors on a device.
+ */
+ @JvmStatic
+ fun vibrateDevice(deviceId: Int, durationMs: Long, intensity: Int, motorIdx: Int): Boolean {
+ val device = InputDevice.getDevice(deviceId) ?: return false
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val vibratorManager = device.vibratorManager
+ val vibratorIds = vibratorManager.vibratorIds
+
+ if (motorIdx == -1) {
+ vibratorIds.forEach {
+ val vibrator = vibratorManager.getVibrator(it)
+ performVibration(vibrator, durationMs, intensity)
+ }
+ } else {
+ val deviceId = vibratorIds.getOrNull(motorIdx) ?: return false
+ val vibrator = vibratorManager.getVibrator(deviceId)
+ performVibration(vibrator, durationMs, intensity)
+ }
+ } else {
+ val vibrator = device.vibrator
+ performVibration(vibrator, durationMs, intensity)
+ }
+
+ return true
+ }
+
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
@@ -588,4 +775,15 @@ object GeodeUtils {
*/
external fun setNextInputTimestamp(timestamp: Long)
external fun setNextInputTimestampInternal(timestamp: Long)
+
+ external fun setNextInputDevice(deviceId: Int, eventSource: Int)
+ external fun inputDeviceAdded(deviceId: Int)
+ external fun inputDeviceChanged(deviceId: Int)
+ external fun inputDeviceRemoved(deviceId: Int)
+
+ /**
+ * Sends all batched joystick events. The final position in each array represents the current position of that axis.
+ * Have fun!
+ */
+ external fun onJoystickEvent(leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt
index 480cb597..eb4a0d5f 100644
--- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt
+++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt
@@ -2,12 +2,14 @@ package org.cocos2dx.lib
import android.app.Activity
import android.content.Context
+import android.hardware.input.InputManager
import android.opengl.GLSurfaceView
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
+import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.Surface
@@ -16,6 +18,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatEditText
import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.utils.GeodeUtils
+import kotlin.math.abs
private const val HANDLER_OPEN_IME_KEYBOARD = 2
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
@@ -58,6 +61,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
var sendTimestampEvents = false
var sendInternalTimestampEvents = false
var manualBackEvents = false
+ var controllerEventsEnabled = false
+ private set
var cocos2dxEditText: AppCompatEditText? = null
set(value) {
@@ -159,6 +164,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
GeodeUtils.setNextInputTimestampInternal(timestamp)
}
+ private fun sendNextEventDetails(event: KeyEvent) {
+ if (controllerEventsEnabled)
+ GeodeUtils.setNextInputDevice(event.deviceId, event.source)
+ }
+
+ private fun sendNextEventDetails(event: MotionEvent) {
+ if (controllerEventsEnabled)
+ GeodeUtils.setNextInputDevice(event.deviceId, event.source)
+ }
+
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
val pointerNumber = motionEvent.pointerCount
val ids = IntArray(pointerNumber)
@@ -178,6 +193,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xDown = xs[0]
val f = ys[0]
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
}
@@ -188,6 +204,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val f2 = xs[0]
val f3 = ys[0]
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
}
@@ -195,6 +212,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
MotionEvent.ACTION_MOVE -> {
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionMove(ids, xs, ys)
}
@@ -202,6 +220,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
MotionEvent.ACTION_CANCEL -> {
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
}
@@ -213,6 +232,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xPointerDown = motionEvent.getX(indexPointerDown)
val y = motionEvent.getY(indexPointerDown)
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
}
@@ -224,6 +244,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xPointerUp = motionEvent.getX(indexPointUp)
val y2 = motionEvent.getY(indexPointUp)
queueEvent {
+ sendNextEventDetails(motionEvent)
sendNextEventTimestamp(timestamp)
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
}
@@ -273,6 +294,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} else {
val currentTime = System.nanoTime()
queueEvent {
+ if (controllerEventsEnabled)
+ GeodeUtils.setNextInputDevice(-1, InputDevice.SOURCE_ANY)
+
sendNextEventTimestamp(currentTime)
GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false)
}
@@ -295,6 +319,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
queueEvent {
+ sendNextEventDetails(event)
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
@@ -318,6 +343,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
queueEvent {
+ sendNextEventDetails(event)
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
@@ -327,6 +353,65 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
}
+ // i'm lazy.
+ // https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
+ private fun getCenteredAxis(
+ event: MotionEvent,
+ device: InputDevice,
+ axis: Int,
+ historyPos: Int
+ ): Float {
+ device.getMotionRange(axis, event.source)?.apply {
+ val value: Float = if (historyPos < 0) {
+ event.getAxisValue(axis)
+ } else {
+ event.getHistoricalAxisValue(axis, historyPos)
+ }
+
+ if (abs(value) > flat) {
+ return value
+ }
+ }
+ return 0.0f
+ }
+
+ private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair {
+ val positionChangesX = ArrayList()
+ val positionChangesY = ArrayList()
+
+ val inputDevice = event.device
+
+ (0 until event.historySize).forEach { i ->
+ val x = getCenteredAxis(event, inputDevice, axisHorizontal, i)
+ val y = getCenteredAxis(event, inputDevice, axisVertical, i)
+ positionChangesX.add(x)
+ positionChangesY.add(y)
+ }
+
+ val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1)
+ val y = getCenteredAxis(event, inputDevice, axisVertical, -1)
+ positionChangesX.add(x)
+ positionChangesY.add(y)
+
+ return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray())
+ }
+
+ private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray {
+ val positionChanges = ArrayList()
+
+ val inputDevice = event.device
+
+ (0 until event.historySize).forEach { i ->
+ val x = getCenteredAxis(event, inputDevice, axis, i)
+ positionChanges.add(x)
+ }
+
+ val x = getCenteredAxis(event, inputDevice, axis, -1)
+ positionChanges.add(x)
+
+ return positionChanges.toFloatArray()
+ }
+
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (!useKeyboardEvents) {
return super.onGenericMotionEvent(event)
@@ -339,6 +424,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
event.eventTimeNanos else event.eventTime * MS_TO_NS
queueEvent {
+ sendNextEventDetails(event)
sendNextEventTimestamp(timestamp)
GeodeUtils.nativeActionScroll(scrollX, scrollY)
}
@@ -346,6 +432,33 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
return true
}
+ if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
+ val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y)
+ val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ)
+ val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y)
+
+ val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER)
+ val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER)
+
+ // i'm sure this is great for performance
+ val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ event.eventTimeNanos else event.eventTime * MS_TO_NS
+
+ queueEvent {
+ sendNextEventDetails(event)
+ sendNextEventTimestamp(timestamp)
+
+ GeodeUtils.onJoystickEvent(
+ motionLeftX, motionLeftY,
+ motionRightX, motionRightY,
+ motionDPadX, motionDPadY,
+ motionLTrigger, motionRTrigger
+ )
+ }
+
+ return true
+ }
+
return super.onGenericMotionEvent(event)
}
@@ -365,4 +478,40 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
fun deleteBackward() {
queueEvent { cocos2dxRenderer.handleDeleteBackward() }
}
+
+ fun enableControllerEvents() {
+ if (controllerEventsEnabled) {
+ return
+ }
+
+ controllerEventsEnabled = true
+
+ val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
+ inputManager.registerInputDeviceListener(InputListener, null)
+ }
+
+ fun disableControllerEvents() {
+ if (!controllerEventsEnabled) {
+ return
+ }
+
+ controllerEventsEnabled = true
+
+ val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
+ inputManager.unregisterInputDeviceListener(InputListener)
+ }
+
+ object InputListener : InputManager.InputDeviceListener {
+ override fun onInputDeviceAdded(deviceId: Int) {
+ GeodeUtils.inputDeviceAdded(deviceId)
+ }
+
+ override fun onInputDeviceChanged(deviceId: Int) {
+ GeodeUtils.inputDeviceChanged(deviceId)
+ }
+
+ override fun onInputDeviceRemoved(deviceId: Int) {
+ GeodeUtils.inputDeviceRemoved(deviceId)
+ }
+ }
}
\ No newline at end of file