From b6ec4366f2da0f537eb48fd9601b505533ca872d Mon Sep 17 00:00:00 2001 From: Giuseppe La Malfa Date: Tue, 15 Sep 2026 15:08:30 +0200 Subject: [PATCH 1/4] android: added option for physical controllers to be handled sideways --- .../butterscotch/android/ButterscotchDroidRunner.kt | 1 + .../butterscotch/android/GameActivity.kt | 1 + .../android/components/GameMetadataFormState.kt | 4 ++++ .../butterscotch/android/components/MetadataForm.kt | 12 ++++++++++++ .../butterscotch/android/library/GameEntry.kt | 1 + .../butterscotch/android/library/GameLibrary.kt | 2 ++ .../android/screens/GameSettingsScreen.kt | 2 ++ .../butterscotch/android/screens/ImportScreen.kt | 3 ++- 8 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/ButterscotchDroidRunner.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/ButterscotchDroidRunner.kt index 9f39a881e..f2e6aecd2 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/ButterscotchDroidRunner.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/ButterscotchDroidRunner.kt @@ -38,6 +38,7 @@ class ButterscotchDroidRunner( val logFile: File, val osType: Int, val enablePhysicalControllers: Boolean, + val physicalControllerSideways: Boolean, val enablePhysicalKeyboard: Boolean, var enableWidescreenHack: Boolean, var postProcessing: GameEntry.PostProcessingSettings, diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GameActivity.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GameActivity.kt index 50a19eee7..a0d01c258 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GameActivity.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GameActivity.kt @@ -135,6 +135,7 @@ class GameActivity : ComponentActivity() { logsFile, entry.runnerOs.nativeValue, entry.enablePhysicalControllers, + entry.physicalControllerSideways, entry.enablePhysicalKeyboard, entry.enableWidescreenHack, entry.postProcessing, diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/GameMetadataFormState.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/GameMetadataFormState.kt index 8a4540788..4aab5fd84 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/GameMetadataFormState.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/GameMetadataFormState.kt @@ -24,6 +24,7 @@ class GameMetadataFormState( initialRunnerOs: GameEntry.RunnerOs, initialEnablePhysicalControllers: Boolean, initialEnablePhysicalKeyboard: Boolean, + initialPhysicalControllerSideways: Boolean, initialEnableWidescreenHack: Boolean, initialPostProcessing: GameEntry.PostProcessingSettings, ) { @@ -34,6 +35,7 @@ class GameMetadataFormState( var runnerOs by mutableStateOf(initialRunnerOs) var enablePhysicalControllers by mutableStateOf(initialEnablePhysicalControllers) var enablePhysicalKeyboard by mutableStateOf(initialEnablePhysicalKeyboard) + var physicalControllerSideways by mutableStateOf(initialPhysicalControllerSideways) var enableWidescreenHack by mutableStateOf(initialEnableWidescreenHack) var postProcessing by mutableStateOf(initialPostProcessing) @@ -51,6 +53,7 @@ fun rememberGameMetadataFormState( runnerOs: GameEntry.RunnerOs, enablePhysicalControllers: Boolean, enablePhysicalKeyboard: Boolean, + physicalControllerSideways: Boolean, enableWidescreenHack: Boolean, postProcessing: GameEntry.PostProcessingSettings = GameEntry.PostProcessingSettings() ): GameMetadataFormState = remember(key) { @@ -61,6 +64,7 @@ fun rememberGameMetadataFormState( initialLandscapeLayout = landscapeLayout, initialRunnerOs = runnerOs, initialEnablePhysicalControllers = enablePhysicalControllers, + initialPhysicalControllerSideways = physicalControllerSideways, initialEnablePhysicalKeyboard = enablePhysicalKeyboard, initialEnableWidescreenHack = enableWidescreenHack, initialPostProcessing = postProcessing diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/MetadataForm.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/MetadataForm.kt index 0f68d6fb5..1110d94ef 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/MetadataForm.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/components/MetadataForm.kt @@ -132,10 +132,22 @@ fun MetadataForm( subtitle = null, checked = state.enablePhysicalControllers, onChange = { state.enablePhysicalControllers = it }, + + ) Spacer(Modifier.height(16.dp)) + if (state.enablePhysicalControllers) + InputToggle( + title = "Turn physical controllers sideways", + subtitle = "Useful for shmups", + checked = state.physicalControllerSideways, + onChange = {state.physicalControllerSideways = it}, + ) + + Spacer(Modifier.height(16.dp)) + InputToggle( title = "Enable physical keyboard", subtitle = null, diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameEntry.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameEntry.kt index 4e6ef05da..7500faf3f 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameEntry.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameEntry.kt @@ -26,6 +26,7 @@ data class GameEntry( val runnerOs: RunnerOs = RunnerOs.WINDOWS, /** When true, physical controllers (Bluetooth/USB gamepads) feed the GML gamepad_* builtins. Default on; turn off for games that misbehave with a controller attached. */ val enablePhysicalControllers: Boolean = true, + val physicalControllerSideways: Boolean = true, /** When true, a physical (USB/Bluetooth) keyboard feeds the GML keyboard_* builtins. Default on; turn off for games that misbehave with a keyboard attached. */ val enablePhysicalKeyboard: Boolean = true, val enableWidescreenHack: Boolean = false, diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameLibrary.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameLibrary.kt index 055639e75..623fcf631 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameLibrary.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/library/GameLibrary.kt @@ -101,6 +101,7 @@ class GameLibrary private constructor( runnerOs: GameEntry.RunnerOs = GameEntry.RunnerOs.WINDOWS, enablePhysicalControllers: Boolean = true, enablePhysicalKeyboard: Boolean = true, + physicalControllerSideways: Boolean = true, enableWidescreenHack: Boolean = false, postProcessing: GameEntry.PostProcessingSettings = GameEntry.PostProcessingSettings() ) { @@ -123,6 +124,7 @@ class GameLibrary private constructor( landscapeLayout = landscapeLayout, runnerOs = runnerOs, enablePhysicalControllers = enablePhysicalControllers, + physicalControllerSideways = physicalControllerSideways, enablePhysicalKeyboard = enablePhysicalKeyboard, enableWidescreenHack = enableWidescreenHack, postProcessing = postProcessing, diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/GameSettingsScreen.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/GameSettingsScreen.kt index 5aadca603..df66ae9ff 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/GameSettingsScreen.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/GameSettingsScreen.kt @@ -49,6 +49,7 @@ fun GameSettingsScreen( runnerOs = entry.runnerOs, enablePhysicalControllers = entry.enablePhysicalControllers, enablePhysicalKeyboard = entry.enablePhysicalKeyboard, + physicalControllerSideways = entry.physicalControllerSideways, enableWidescreenHack = entry.enableWidescreenHack, postProcessing = entry.postProcessing ) @@ -63,6 +64,7 @@ fun GameSettingsScreen( if (state.portraitLayout != entry.portraitLayout || state.landscapeLayout != entry.landscapeLayout) gameLibrary.update(entry.id) { it.copy(portraitLayout = state.portraitLayout, landscapeLayout = state.landscapeLayout) } if (state.runnerOs != entry.runnerOs) gameLibrary.update(entry.id) { it.copy(runnerOs = state.runnerOs) } if (state.enablePhysicalControllers != entry.enablePhysicalControllers) gameLibrary.update(entry.id) { it.copy(enablePhysicalControllers = state.enablePhysicalControllers) } + if (state.physicalControllerSideways != entry.physicalControllerSideways) gameLibrary.update(entry.id) {it.copy(physicalControllerSideways = state.physicalControllerSideways)} if (state.enablePhysicalKeyboard != entry.enablePhysicalKeyboard) gameLibrary.update(entry.id) { it.copy(enablePhysicalKeyboard = state.enablePhysicalKeyboard) } if (state.enableWidescreenHack != entry.enableWidescreenHack) gameLibrary.update(entry.id) { it.copy(enableWidescreenHack = state.enableWidescreenHack) } if (state.postProcessing != entry.postProcessing) gameLibrary.update(entry.id) { it.copy(postProcessing = state.postProcessing) } diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/ImportScreen.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/ImportScreen.kt index 22382c232..829c0131a 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/ImportScreen.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/screens/ImportScreen.kt @@ -335,8 +335,9 @@ private fun ConfigurePane( landscapeLayout = LayoutLibrary.DEFAULT_LANDSCAPE_LAYOUT, runnerOs = GameEntry.RunnerOs.WINDOWS, enablePhysicalControllers = true, + physicalControllerSideways = false, enablePhysicalKeyboard = true, - enableWidescreenHack = false + enableWidescreenHack = false, ) MetadataForm( From 49e151363b1db8540e741c82d06e0a0c82f39593 Mon Sep 17 00:00:00 2001 From: Giuseppe La Malfa Date: Tue, 15 Sep 2026 15:08:59 +0200 Subject: [PATCH 2/4] android: GamepadRouter implementation for sideways physical controller layout --- .../butterscotch/android/GamepadRouter.kt | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt index a60692ec0..a8e0eb5dc 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt @@ -97,13 +97,56 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { // Analog sticks: forward raw, the runner deadzones on read. Left = X/Y, right = Z/RZ (the // common Android mapping; controllers that expose the right stick on RX/RY won't drive it). - sendAxis(slot, state, AXIS_LH, event.getAxisValue(MotionEvent.AXIS_X)) - sendAxis(slot, state, AXIS_LV, event.getAxisValue(MotionEvent.AXIS_Y)) - sendAxis(slot, state, AXIS_RH, event.getAxisValue(MotionEvent.AXIS_Z)) - sendAxis(slot, state, AXIS_RV, event.getAxisValue(MotionEvent.AXIS_RZ)) + if (!runner.physicalControllerSideways){ + sendAxis(slot, state, AXIS_LH, event.getAxisValue(MotionEvent.AXIS_X)) + sendAxis(slot, state, AXIS_LV, event.getAxisValue(MotionEvent.AXIS_Y)) + sendAxis(slot, state, AXIS_RH, event.getAxisValue(MotionEvent.AXIS_Z)) + sendAxis(slot, state, AXIS_RV, event.getAxisValue(MotionEvent.AXIS_RZ)) + + // Many controllers report the dpad as a hat axis rather than dpad keycodes. + updateHat(slot, state, event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y)) + + } else { + // Sideways controller movement + sendAxis(slot, state, AXIS_LH, event.getAxisValue(MotionEvent.AXIS_Y)) + sendAxis(slot, state, AXIS_LV, -event.getAxisValue(MotionEvent.AXIS_X)) + sendAxis(slot, state, AXIS_RH, event.getAxisValue(MotionEvent.AXIS_RZ)) + sendAxis(slot, state, AXIS_RV, -event.getAxisValue(MotionEvent.AXIS_Z)) + + // Dpad as buttons + val xhat = signOf(event.getAxisValue(MotionEvent.AXIS_HAT_X)) + val yhat = signOf(event.getAxisValue(MotionEvent.AXIS_HAT_Y)) + + if (xhat != state.lastHatX) + { + if(xhat < 0) + runner.onGamepadButton(slot, 0, true) + else + runner.onGamepadButton(slot, 0, false) + + if(xhat > 0) + runner.onGamepadButton(slot, 3, true) + else + runner.onGamepadButton(slot, 3, false) + } + + if(yhat != state.lastHatY) + { + if (yhat < 0) + runner.onGamepadButton(slot, 2, true) + else + runner.onGamepadButton(slot, 2, false) + + if (yhat > 0) + runner.onGamepadButton(slot, 1, true) + else + runner.onGamepadButton(slot, 1, false) + } + + state.lastHatX = xhat + state.lastHatY = yhat + } - // Many controllers report the dpad as a hat axis rather than dpad keycodes. - updateHat(slot, state, event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y)) // Analog triggers -> digital L2/R2. Different controllers use LTRIGGER/RTRIGGER or BRAKE/GAS. val left = maxOf(event.getAxisValue(MotionEvent.AXIS_LTRIGGER), event.getAxisValue(MotionEvent.AXIS_BRAKE)) From bc448d993d72d4adf730485d7ed3adeed562d8d6 Mon Sep 17 00:00:00 2001 From: Giuseppe La Malfa Date: Tue, 15 Sep 2026 23:34:03 +0200 Subject: [PATCH 3/4] fixup! android: GamepadRouter implementation for sideways physical controller layout --- .../perfectdreams/butterscotch/android/GamepadRouter.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt index a8e0eb5dc..04d785ad7 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt @@ -121,12 +121,12 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { { if(xhat < 0) runner.onGamepadButton(slot, 0, true) - else + else if (state.lastHatX < 0) runner.onGamepadButton(slot, 0, false) if(xhat > 0) runner.onGamepadButton(slot, 3, true) - else + else if (state.lastHatX > 0) runner.onGamepadButton(slot, 3, false) } @@ -134,12 +134,12 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { { if (yhat < 0) runner.onGamepadButton(slot, 2, true) - else + else if(state.lastHatY < 0) runner.onGamepadButton(slot, 2, false) if (yhat > 0) runner.onGamepadButton(slot, 1, true) - else + else if(state.lastHatY > 0) runner.onGamepadButton(slot, 1, false) } From 2f4ae56aaa7efbeaeec139af9c9bb8c58b8ab817 Mon Sep 17 00:00:00 2001 From: Giuseppe La Malfa Date: Wed, 16 Sep 2026 08:25:10 +0200 Subject: [PATCH 4/4] fixup! fixup! android: GamepadRouter implementation for sideways physical controller layout --- game | 1 + .../butterscotch/android/GamepadRouter.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 120000 game diff --git a/game b/game new file mode 120000 index 000000000..7307a2283 --- /dev/null +++ b/game @@ -0,0 +1 @@ +/home/glitchedcode/.config/retroarch/ZeroRanger 29092022 B/ \ No newline at end of file diff --git a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt index 04d785ad7..80b069f4d 100644 --- a/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt +++ b/src/android/frontend/app/src/main/kotlin/net/perfectdreams/butterscotch/android/GamepadRouter.kt @@ -76,7 +76,7 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { fun handleKeyEvent(event: KeyEvent): Boolean { val slot = ensureSlot(event.deviceId, event.device) if (slot < 0) return false - val button = keyCodeToButton(event.keyCode) + val button = keyCodeToButton(event.keyCode, runner) if (button < 0) return false when (event.action) { // GML uses an edge model, so key-repeat (repeatCount > 0) is ignored: the button is @@ -227,7 +227,7 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { } // Android KeyEvent keycode -> canonical button index, or -1 to leave the event alone. - private fun keyCodeToButton(keyCode: Int): Int = when (keyCode) { + private fun keyCodeToButton(keyCode: Int, runner: ButterscotchDroidRunner): Int = when (keyCode) { KeyEvent.KEYCODE_BUTTON_A -> Gamepad.Button.FACE1.index KeyEvent.KEYCODE_BUTTON_B -> Gamepad.Button.FACE2.index KeyEvent.KEYCODE_BUTTON_X -> Gamepad.Button.FACE3.index @@ -240,10 +240,10 @@ class GamepadRouter(private val runner: ButterscotchDroidRunner) { KeyEvent.KEYCODE_BUTTON_START -> Gamepad.Button.START.index KeyEvent.KEYCODE_BUTTON_THUMBL -> Gamepad.Button.STICK_L.index KeyEvent.KEYCODE_BUTTON_THUMBR -> Gamepad.Button.STICK_R.index - KeyEvent.KEYCODE_DPAD_UP -> Gamepad.Button.DPAD_UP.index - KeyEvent.KEYCODE_DPAD_DOWN -> Gamepad.Button.DPAD_DOWN.index - KeyEvent.KEYCODE_DPAD_LEFT -> Gamepad.Button.DPAD_LEFT.index - KeyEvent.KEYCODE_DPAD_RIGHT -> Gamepad.Button.DPAD_RIGHT.index + KeyEvent.KEYCODE_DPAD_UP if !runner.physicalControllerSideways -> Gamepad.Button.DPAD_UP.index + KeyEvent.KEYCODE_DPAD_DOWN if !runner.physicalControllerSideways -> Gamepad.Button.DPAD_DOWN.index + KeyEvent.KEYCODE_DPAD_LEFT if !runner.physicalControllerSideways -> Gamepad.Button.DPAD_LEFT.index + KeyEvent.KEYCODE_DPAD_RIGHT if !runner.physicalControllerSideways -> Gamepad.Button.DPAD_RIGHT.index KeyEvent.KEYCODE_BUTTON_MODE -> Gamepad.Button.HOME.index else -> -1 }