diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2606c146..86d9b279 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,7 @@
+
+
+
1.77f
}
-class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
+class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener, InputManager.InputDeviceListener {
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
private var mIsRunning = false
private var mIsOnPause = false
@@ -77,6 +82,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mScreenZoom = 1.0f
private var mScreenZoomFit = false
+ private var mGamepads = mutableListOf()
+ private lateinit var mInputManager: InputManager
+
override fun onCreate(savedInstanceState: Bundle?) {
setupUIState()
FMOD.init(this)
@@ -129,6 +137,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
})
mGLSurfaceView?.manualBackEvents = true
+
+ mInputManager = getSystemService(INPUT_SERVICE) as InputManager
+ mInputManager.registerInputDeviceListener(this, null)
+ updateControllerDeviceIDs()
}
private fun createVersionFile() {
@@ -511,6 +523,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
override fun onDestroy() {
super.onDestroy()
FMOD.close()
+ mInputManager.unregisterInputDeviceListener(this)
}
private fun resumeGame() {
@@ -628,6 +641,133 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
}
+ private fun updateControllerDeviceIDs() {
+ mGamepads.clear()
+
+ // basically taken from documentation
+ val deviceIDs = InputDevice.getDeviceIds()
+ for (id in deviceIDs) {
+ val device = InputDevice.getDevice(id) ?: continue
+
+ if (device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD
+ || device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
+ mGamepads.add(GeodeUtils.Gamepad(device.id))
+ }
+ }
+ }
+
+ override fun onInputDeviceAdded(deviceID: Int) {
+ updateControllerDeviceIDs()
+ if (GeodeUtils.controllerCallbacksEnabled()) {
+ val index = mGamepads.indexOfFirst { it.mDeviceID == deviceID }
+ Handler(mainLooper).post { GeodeUtils.setControllerConnected(index, true) }
+ }
+ }
+
+ override fun onInputDeviceRemoved(deviceID: Int) {
+ if (GeodeUtils.controllerCallbacksEnabled()) {
+ val index = mGamepads.indexOfFirst { it.mDeviceID == deviceID }
+ Handler(mainLooper).post { GeodeUtils.setControllerConnected(index, false) }
+ }
+ updateControllerDeviceIDs()
+ }
+
+ override fun onInputDeviceChanged(deviceID: Int) {}
+
+ override fun dispatchGenericMotionEvent(event: MotionEvent?): Boolean {
+ event ?: return super.dispatchGenericMotionEvent(null)
+
+ val index = mGamepads.indexOfFirst { it.mDeviceID == event.deviceId }
+ if (index == -1) return super.dispatchGenericMotionEvent(null)
+ val gamepad = mGamepads[index]
+
+ fun processJoystick(event: MotionEvent, index: Int) {
+ if (index < 0) {
+ gamepad.mJoyLeftX = event.getAxisValue(MotionEvent.AXIS_X)
+ gamepad.mJoyLeftY = -event.getAxisValue(MotionEvent.AXIS_Y)
+ gamepad.mJoyRightX = event.getAxisValue(MotionEvent.AXIS_Z) // wtf is axis z and rz
+ gamepad.mJoyRightY = -event.getAxisValue(MotionEvent.AXIS_RZ)
+ gamepad.mTriggerZL = event.getAxisValue(MotionEvent.AXIS_LTRIGGER)
+ gamepad.mTriggerZR = event.getAxisValue(MotionEvent.AXIS_RTRIGGER)
+ gamepad.mButtonUp = event.getAxisValue(MotionEvent.AXIS_HAT_Y) < 0.0f
+ gamepad.mButtonDown = event.getAxisValue(MotionEvent.AXIS_HAT_Y) > 0.0f
+ gamepad.mButtonLeft = event.getAxisValue(MotionEvent.AXIS_HAT_X) < 0.0f
+ gamepad.mButtonRight = event.getAxisValue(MotionEvent.AXIS_HAT_X) > 0.0f
+ } else {
+ gamepad.mJoyLeftX = event.getHistoricalAxisValue(MotionEvent.AXIS_X, index)
+ gamepad.mJoyLeftY = -event.getHistoricalAxisValue(MotionEvent.AXIS_Y, index)
+ gamepad.mJoyRightX = event.getHistoricalAxisValue(MotionEvent.AXIS_Z, index)
+ gamepad.mJoyRightY = -event.getHistoricalAxisValue(MotionEvent.AXIS_RZ, index)
+ gamepad.mTriggerZL = event.getHistoricalAxisValue(MotionEvent.AXIS_LTRIGGER, index)
+ gamepad.mTriggerZR = event.getHistoricalAxisValue(MotionEvent.AXIS_RTRIGGER, index)
+ gamepad.mButtonUp = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, index) < 0.0f
+ gamepad.mButtonDown = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, index) > 0.0f
+ gamepad.mButtonLeft = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, index) < 0.0f
+ gamepad.mButtonRight = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, index) > 0.0f
+ }
+ }
+
+ // taken from documentation - android batches joystick events for efficiency
+
+ // Process the movements starting from the
+ // earliest historical position in the batch
+ (0 until event.historySize).forEach { i ->
+ // Process the event at historical position i
+ processJoystick(event, i)
+ }
+
+ // Process the current movement sample in the batch (position -1)
+ processJoystick(event, -1)
+
+ // call callback in main thread
+ Handler(mainLooper).post {
+ if (GeodeUtils.controllerCallbacksEnabled()) GeodeUtils.setControllerState(index, gamepad)
+ }
+
+ return true;
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ val index = mGamepads.indexOfFirst { it.mDeviceID == event.deviceId }
+ if (index == -1) return super.dispatchKeyEvent(event)
+ val gamepad = mGamepads[index]
+
+ val changed = when (event.keyCode) {
+ KeyEvent.KEYCODE_BUTTON_A -> { gamepad.mButtonA = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_B -> { gamepad.mButtonB = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_X -> { gamepad.mButtonX = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_Y -> { gamepad.mButtonY = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_START -> { gamepad.mButtonStart = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_SELECT -> { gamepad.mButtonSelect = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_L1 -> { gamepad.mButtonL = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_R1 -> { gamepad.mButtonR = event.action == KeyEvent.ACTION_DOWN; true }
+ // zl/zr/d-pad don't actually function as a button on my controllers but android documentation says to keep it in for compatibility
+ KeyEvent.KEYCODE_BUTTON_L2 -> { gamepad.mTriggerZL = if (event.action == KeyEvent.ACTION_DOWN) 1.0f else 0.0f; true }
+ KeyEvent.KEYCODE_BUTTON_R2 -> { gamepad.mTriggerZR = if (event.action == KeyEvent.ACTION_DOWN) 1.0f else 0.0f; true }
+ KeyEvent.KEYCODE_DPAD_UP -> { gamepad.mButtonUp = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_DPAD_DOWN -> { gamepad.mButtonDown = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_DPAD_LEFT -> { gamepad.mButtonLeft = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_DPAD_RIGHT -> { gamepad.mButtonRight = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> { gamepad.mButtonJoyLeft = event.action == KeyEvent.ACTION_DOWN; true }
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> { gamepad.mButtonJoyRight = event.action == KeyEvent.ACTION_DOWN; true }
+ else -> false
+ }
+
+ // call callback in main thread
+ if (changed) {
+ Handler(mainLooper).post {
+ if (GeodeUtils.controllerCallbacksEnabled()) GeodeUtils.setControllerState(index, gamepad)
+ }
+
+ return true
+ }
+
+ return super.dispatchKeyEvent(event)
+ }
+
+ fun getGamepad(id: Int) = mGamepads.getOrNull(id)
+ fun getGamepadCount() = mGamepads.size
+
class EGLConfigChooser : GLSurfaceView.EGLConfigChooser {
// this comes from EGL14, but is unavailable on EGL10
// also EGL14 is incompatible with EGL10. so whatever
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 12578802..fe348712 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,8 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.hardware.lights.LightState
+import android.hardware.lights.LightsRequest
import android.net.Uri
import android.os.Build
import android.os.Environment
@@ -16,6 +18,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
@@ -26,6 +29,7 @@ import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import com.geode.launcher.BuildConfig
+import com.geode.launcher.GeometryDashActivity
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
@@ -52,6 +56,8 @@ object GeodeUtils {
private var afterRequestPermissions: (() -> Unit)? = null
private var afterRequestPermissionsFailure: (() -> Unit)? = null
+ private var mControllerCallbacksEnabled: Boolean = false
+
fun setContext(activity: AppCompatActivity) {
this.activity = WeakReference(activity)
openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri ->
@@ -572,6 +578,106 @@ object GeodeUtils {
.launchUrl(activity, url.toUri())
}
+ @JvmStatic
+ fun getControllerCount(): Int {
+ val act = activity.get()
+ return if (act is GeometryDashActivity) act.getGamepadCount() else 0
+ }
+
+ /**
+ * Enables calling of the controller callbacks - they **must** be defined in native functions before this is called
+ * @see setControllerState
+ * @see setControllerConnected
+ */
+ @JvmStatic
+ fun enableControllerCallbacks() { mControllerCallbacksEnabled = true }
+ @JvmStatic
+ fun controllerCallbacksEnabled() = mControllerCallbacksEnabled
+
+ /**
+ * Whether the **device** supports vibration or lighting effects - not necessarily if the controller can or not.
+ */
+ @JvmStatic
+ fun supportsControllerExtendedFeatures(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+
+ @JvmStatic
+ fun setControllerVibration(id: Int, duration: Long, left: Int, right: Int) {
+ val act = activity.get()
+ if (act is GeometryDashActivity) {
+ act.getGamepad(id)?.setVibration(duration, left, right)
+ }
+ }
+
+ @JvmStatic
+ fun setControllerColor(id: Int, color: Int) {
+ val act = activity.get()
+ if (act is GeometryDashActivity) {
+ act.getGamepad(id)?.setColor(color)
+ }
+ }
+
+ class Gamepad(deviceID: Int) {
+ var mButtonA: Boolean = false
+ var mButtonB: Boolean = false
+ var mButtonX: Boolean = false
+ var mButtonY: Boolean = false
+ var mButtonStart: Boolean = false
+ var mButtonSelect: Boolean = false
+ var mButtonL: Boolean = false
+ var mButtonR: Boolean = false
+ var mTriggerZL: Float = 0.0f
+ var mTriggerZR: Float = 0.0f
+ var mButtonUp: Boolean = false
+ var mButtonDown: Boolean = false
+ var mButtonLeft: Boolean = false
+ var mButtonRight: Boolean = false
+ var mButtonJoyLeft: Boolean = false
+ var mButtonJoyRight: Boolean = false
+
+ var mJoyLeftX: Float = 0.0f
+ var mJoyLeftY: Float = 0.0f
+ var mJoyRightX: Float = 0.0f
+ var mJoyRightY: Float = 0.0f
+
+ var mDeviceID: Int = deviceID
+
+ fun setVibration(duration: Long, left: Int, right: Int) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
+
+ val device = InputDevice.getDevice(mDeviceID) ?: return
+ val manager = device.vibratorManager
+ val ids = manager.vibratorIds
+
+ if (ids.size == 1) {
+ manager.getVibrator(ids[0]).vibrate(VibrationEffect.createOneShot(duration, (left + right) / 2))
+ }
+
+ if (ids.size == 2) {
+ manager.getVibrator(ids[0]).vibrate(VibrationEffect.createOneShot(duration, left))
+ manager.getVibrator(ids[1]).vibrate(VibrationEffect.createOneShot(duration, right))
+ }
+ }
+
+ fun setColor(color: Int) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
+
+ val device = InputDevice.getDevice(mDeviceID) ?: return
+ val manager = device.lightsManager
+ val request = LightsRequest.Builder()
+
+ for (light in manager.lights) {
+ request.addLight(
+ light,
+ LightState.Builder()
+ .setColor(color)
+ .build()
+ )
+ }
+
+ manager.openSession().requestLights(request.build())
+ }
+ }
+
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
@@ -583,5 +689,13 @@ object GeodeUtils {
* @see reportPlatformCapability
*/
external fun setNextInputTimestamp(timestamp: Long)
+
+ /**
+ * Gives the state of the current controller at the index, whenever it updates.
+ * @see enableControllerCallbacks
+ */
+ external fun setControllerState(index: Int, gamepad: Gamepad)
+ external fun setControllerConnected(index: Int, connected: Boolean)
+
external fun setNextInputTimestampInternal(timestamp: Long)
}
\ No newline at end of file