beginning of touch controls

This commit is contained in:
izzy2lost
2026-02-09 19:29:00 -05:00
parent e6de9da878
commit a1c5f41807
11 changed files with 1199 additions and 31 deletions
+268
View File
@@ -0,0 +1,268 @@
# On-Screen Controller
This implementation provides a virtual Xbox controller overlay for touchscreen devices.
## Features
### Controller Layout
Based on the original Xbox controller, the on-screen controller includes:
**Face Buttons (Right Side)**
- A Button (Green) - Bottom
- B Button (Red) - Right
- X Button (Blue) - Left
- Y Button (Yellow) - Top
**D-Pad (Left Side)**
- Up, Down, Left, Right directional buttons
**Analog Sticks**
- Left Stick - Lower left position
- Right Stick - Lower right position
- Both sticks support 360-degree movement with dead zone
- Click-able (L3/R3 functionality)
**Shoulder Buttons**
- Left Trigger (LT) - Top left
- Right Trigger (RT) - Top right
- Left Bumper (LB) - Below left trigger
- Right Bumper (RB) - Below right trigger
**Center Buttons**
- Start Button - Right of center
- Back Button - Left of center
**Original Xbox Buttons**
- Black Button - Upper right area
- White Button - Upper right area
## Usage
### Automatic Behavior
The on-screen controller is **enabled by default** and will:
- ✅ Show automatically when no physical controller is connected
- ✅ Hide automatically when a physical controller is plugged in
- ✅ Reappear automatically when the physical controller is disconnected
This provides a seamless experience where users always have a way to control the game.
### Physical Controller Detection
The system monitors for:
- USB game controllers
- Bluetooth game controllers
- Any device with `SOURCE_GAMEPAD` or `SOURCE_JOYSTICK` input sources
When any physical controller is detected, the on-screen controller automatically hides to avoid cluttering the screen.
### Manual Control
You can still manually control the on-screen controller if needed:
```kotlin
// In MainActivity
val mainActivity = this as MainActivity
// Show controller
mainActivity.showOnScreenController()
// Hide controller
mainActivity.hideOnScreenController()
// Toggle visibility
mainActivity.toggleOnScreenController()
// Force recheck for physical controllers
mainActivity.forceUpdateControllerVisibility()
```
### User Preference Override
Users can override the automatic behavior through settings:
Controller preferences are managed through `ControllerSettings`:
```kotlin
val settings = ControllerSettings(context)
// Enable/disable on-screen controller (default: true)
settings.showOnScreenController = true
// Adjust opacity (0.0 - 1.0)
settings.controllerOpacity = 0.7f
// Adjust scale (0.5 - 2.0)
settings.controllerScale = 1.0f
// Enable/disable vibration feedback
settings.vibrationEnabled = true
```
## Implementation Details
### Automatic Controller Detection
The `MainActivity` implements `InputManager.InputDeviceListener` to monitor for controller connections:
1. **On Startup**: Scans for existing connected controllers
2. **On Device Added**: Detects when a controller is plugged in (USB/Bluetooth)
3. **On Device Removed**: Detects when a controller is disconnected
4. **On Device Changed**: Handles configuration changes
The detection logic identifies game controllers by checking for:
- `InputDevice.SOURCE_GAMEPAD` - Standard gamepad devices
- `InputDevice.SOURCE_JOYSTICK` - Joystick devices
### Components
1. **OnScreenController.kt**
- Custom View that renders the controller overlay
- Handles multi-touch input for simultaneous button presses
- Provides visual feedback for button states
- Manages analog stick positioning with dead zones
2. **ControllerInputBridge.kt**
- Bridges on-screen controller events to SDL input system
- Maps virtual buttons to SDL controller buttons
- Converts analog stick positions to axis events
- Translates to Android KeyEvent codes
3. **ControllerSettings.kt**
- Manages user preferences for controller behavior
- Persists settings using SharedPreferences
### Input Mapping
The controller maps to standard SDL controller inputs:
| On-Screen Button | SDL Button | KeyEvent |
|-----------------|------------|----------|
| A | SDL_CONTROLLER_BUTTON_A | KEYCODE_BUTTON_A |
| B | SDL_CONTROLLER_BUTTON_B | KEYCODE_BUTTON_B |
| X | SDL_CONTROLLER_BUTTON_X | KEYCODE_BUTTON_X |
| Y | SDL_CONTROLLER_BUTTON_Y | KEYCODE_BUTTON_Y |
| D-Pad Up | SDL_CONTROLLER_BUTTON_DPAD_UP | KEYCODE_DPAD_UP |
| D-Pad Down | SDL_CONTROLLER_BUTTON_DPAD_DOWN | KEYCODE_DPAD_DOWN |
| D-Pad Left | SDL_CONTROLLER_BUTTON_DPAD_LEFT | KEYCODE_DPAD_LEFT |
| D-Pad Right | SDL_CONTROLLER_BUTTON_DPAD_RIGHT | KEYCODE_DPAD_RIGHT |
| Left Bumper | SDL_CONTROLLER_BUTTON_LEFTSHOULDER | KEYCODE_BUTTON_L1 |
| Right Bumper | SDL_CONTROLLER_BUTTON_RIGHTSHOULDER | KEYCODE_BUTTON_R1 |
| Left Trigger | Axis Event | KEYCODE_BUTTON_L2 |
| Right Trigger | Axis Event | KEYCODE_BUTTON_R2 |
| Start | SDL_CONTROLLER_BUTTON_START | KEYCODE_BUTTON_START |
| Back | SDL_CONTROLLER_BUTTON_BACK | KEYCODE_BUTTON_SELECT |
| Left Stick | Axis Events (X/Y) | - |
| Right Stick | Axis Events (X/Y) | - |
| L3 (Left Stick Click) | SDL_CONTROLLER_BUTTON_LEFTSTICK | KEYCODE_BUTTON_THUMBL |
| R3 (Right Stick Click) | SDL_CONTROLLER_BUTTON_RIGHTSTICK | KEYCODE_BUTTON_THUMBR |
### Analog Stick Behavior
- **Dead Zone**: 20% of stick radius (configurable)
- **Range**: -1.0 to 1.0 for both X and Y axes
- **Clamping**: Movement is constrained to circular boundary
- **Multi-touch**: Each stick can be controlled independently
## Customization
### Adjusting Button Positions
Edit `initializeControls()` in `OnScreenController.kt` to modify button positions:
```kotlin
// Example: Move A button
buttons[Button.A] = ButtonState(
PointF(w * 0.85f, h * 0.5f), // x, y as percentage of screen
faceButtonRadius
)
```
### Changing Button Colors
Modify `getButtonColor()` and `getButtonPressedColor()` methods:
```kotlin
private fun getButtonColor(button: Button): Int {
return when (button) {
Button.A -> Color.argb(150, 100, 200, 100) // ARGB values
// ... other buttons
}
}
```
### Adjusting Sizes
Button and stick sizes are calculated as percentages of screen width:
```kotlin
val faceButtonRadius = w * 0.04f // 4% of screen width
val stickRadius = w * 0.08f // 8% of screen width
```
## Future Enhancements
Potential improvements:
- Haptic feedback on button press
- Customizable button layouts
- Save/load custom controller configurations
- Opacity and scale adjustments via UI
- Button remapping
- Portrait/landscape specific layouts
- Gesture support (swipe for quick actions)
## Testing
To test the on-screen controller:
1. **Default Behavior**
- Build and run the app on a touchscreen device
- On-screen controller should be visible by default
- Test each button for visual feedback and input
2. **Physical Controller Detection**
- Connect a USB or Bluetooth game controller
- On-screen controller should automatically hide
- Disconnect the controller
- On-screen controller should automatically reappear
3. **Multi-touch Testing**
- Test analog sticks for smooth movement
- Test multi-touch (e.g., move both sticks simultaneously)
- Press multiple buttons at once
4. **Game Integration**
- Verify input is received by the game/emulator
- Test all buttons map correctly to game actions
## Troubleshooting
**Controller not visible on startup**
- Check that no physical controller is connected
- Verify the view is added to the layout hierarchy
- Check `ControllerSettings.showOnScreenController` preference
**Controller doesn't hide when physical controller connected**
- Verify the physical controller is recognized as a gamepad
- Check logcat for InputDevice events
- Call `forceUpdateControllerVisibility()` to manually trigger detection
**Controller doesn't reappear when physical controller disconnected**
- Check that `InputDeviceListener` is properly registered
- Verify `onInputDeviceRemoved()` is being called
- Check for any exceptions in the listener callbacks
**Buttons not responding**
- Ensure `ControllerInputBridge` is properly connected
- Check that SDL input system is initialized
- Verify touch events are not being intercepted by other views
**Analog sticks not working**
- Confirm axis events are being sent to SDL
- Check dead zone settings
- Verify stick position calculations
**Performance issues**
- Reduce controller opacity
- Optimize `onDraw()` method
- Consider using hardware acceleration
@@ -76,6 +76,10 @@ __attribute__((used)) static void *xemu_android_force_smbus_storage_ref =
extern void xemu_android_force_usb_hub_link(void);
__attribute__((used)) static void *xemu_android_force_usb_hub_ref =
(void *)&xemu_android_force_usb_hub_link;
/* Force-link the Xbox USB gamepad registration unit from the static lib. */
extern void xemu_android_force_xid_gamepad_link(void);
__attribute__((used)) static void *xemu_android_force_xid_gamepad_ref =
(void *)&xemu_android_force_xid_gamepad_link;
#define XEMU_ANDROID_ACCEL_CPU_TYPE "accel-i386-cpu"
@@ -1,6 +1,7 @@
#include "qemu/osdep.h"
#include <SDL_filesystem.h>
#include <SDL_gamecontroller.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
@@ -337,14 +338,127 @@ void remove_net_nat_forward_ports(unsigned int index)
bool xemu_settings_load_gamepad_mapping(const char *guid,
GamepadMappings **mapping)
{
(void)guid;
if (!mapping) {
return false;
}
*mapping = NULL;
if (!guid || *guid == '\0') {
return false;
}
unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count;
for (unsigned int i = 0; i < gamepad_mappings_count; ++i) {
GamepadMappings *entry = &g_config.input.gamepad_mappings[i];
if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) {
continue;
}
// Preserve old behavior: global vibration off disables rumble.
if (!g_config.input.allow_vibration) {
entry->enable_rumble = false;
}
*mapping = entry;
return false;
}
auto apply_default_controller_mapping = [](GamepadMappings *entry) {
entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A;
entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B;
entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X;
entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y;
entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK;
entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE;
entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START;
entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK;
entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
entry->controller_mapping.rshoulder =
SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
entry->controller_mapping.dpad_right =
SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX;
entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY;
entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX;
entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY;
entry->controller_mapping.axis_trigger_left =
SDL_CONTROLLER_AXIS_TRIGGERLEFT;
entry->controller_mapping.axis_trigger_right =
SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
entry->controller_mapping.invert_axis_left_x = false;
entry->controller_mapping.invert_axis_left_y = false;
entry->controller_mapping.invert_axis_right_x = false;
entry->controller_mapping.invert_axis_right_y = false;
};
const unsigned int old_count = g_config.input.gamepad_mappings_count;
const unsigned int new_count = old_count + 1;
GamepadMappings *new_mappings = static_cast<GamepadMappings *>(realloc(
g_config.input.gamepad_mappings, sizeof(GamepadMappings) * new_count));
if (!new_mappings) {
__android_log_print(ANDROID_LOG_ERROR, "xemu-android",
"Failed to allocate gamepad mapping for %s", guid);
return false;
}
g_config.input.gamepad_mappings = new_mappings;
g_config.input.gamepad_mappings_count = new_count;
GamepadMappings *entry = &g_config.input.gamepad_mappings[old_count];
memset(entry, 0, sizeof(*entry));
entry->gamepad_id = strdup(guid);
entry->enable_rumble = g_config.input.allow_vibration;
apply_default_controller_mapping(entry);
*mapping = entry;
return true;
}
void xemu_settings_reset_controller_mapping(const char *guid)
{
(void)guid;
if (!guid || *guid == '\0') {
return;
}
unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count;
for (unsigned int i = 0; i < gamepad_mappings_count; ++i) {
GamepadMappings *entry = &g_config.input.gamepad_mappings[i];
if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) {
continue;
}
entry->enable_rumble = g_config.input.allow_vibration;
entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A;
entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B;
entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X;
entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y;
entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK;
entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE;
entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START;
entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK;
entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
entry->controller_mapping.rshoulder = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
entry->controller_mapping.dpad_right = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX;
entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY;
entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX;
entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY;
entry->controller_mapping.axis_trigger_left = SDL_CONTROLLER_AXIS_TRIGGERLEFT;
entry->controller_mapping.axis_trigger_right = SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
entry->controller_mapping.invert_axis_left_x = false;
entry->controller_mapping.invert_axis_left_y = false;
entry->controller_mapping.invert_axis_right_x = false;
entry->controller_mapping.invert_axis_right_y = false;
return;
}
}
void xemu_settings_reset_keyboard_mapping(void)
@@ -0,0 +1,105 @@
package com.izzy2lost.x1box
import android.view.KeyEvent
import org.libsdl.app.SDLControllerManager
/**
* Bridge between on-screen controller and SDL input system
*/
class ControllerInputBridge : OnScreenController.ControllerListener {
companion object {
// Virtual device ID for on-screen controller
const val VIRTUAL_DEVICE_ID = -2
// Axis indices for SDL
const val AXIS_LEFT_X = 0
const val AXIS_LEFT_Y = 1
const val AXIS_RIGHT_X = 2
const val AXIS_RIGHT_Y = 3
const val AXIS_LEFT_TRIGGER = 4
const val AXIS_RIGHT_TRIGGER = 5
}
override fun onButtonPressed(button: OnScreenController.Button) {
try {
val keyCode = getKeyCodeForButton(button)
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
android.util.Log.e("ControllerBridge", "Error on button press: ${e.message}")
}
}
override fun onButtonReleased(button: OnScreenController.Button) {
try {
val keyCode = getKeyCodeForButton(button)
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
android.util.Log.e("ControllerBridge", "Error on button release: ${e.message}")
}
}
override fun onStickMoved(stick: OnScreenController.Stick, x: Float, y: Float) {
try {
when (stick) {
OnScreenController.Stick.LEFT -> {
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_X, x)
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_Y, y)
}
OnScreenController.Stick.RIGHT -> {
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_X, x)
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_Y, y)
}
}
} catch (e: Exception) {
android.util.Log.e("ControllerBridge", "Error on stick move: ${e.message}")
}
}
override fun onStickPressed(stick: OnScreenController.Stick) {
try {
val keyCode = when (stick) {
OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL
OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR
}
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
android.util.Log.e("ControllerBridge", "Error on stick press: ${e.message}")
}
}
override fun onStickReleased(stick: OnScreenController.Stick) {
try {
val keyCode = when (stick) {
OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL
OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR
}
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
android.util.Log.e("ControllerBridge", "Error on stick release: ${e.message}")
}
}
private fun getKeyCodeForButton(button: OnScreenController.Button): Int {
return when (button) {
OnScreenController.Button.A -> KeyEvent.KEYCODE_BUTTON_A
OnScreenController.Button.B -> KeyEvent.KEYCODE_BUTTON_B
OnScreenController.Button.X -> KeyEvent.KEYCODE_BUTTON_X
OnScreenController.Button.Y -> KeyEvent.KEYCODE_BUTTON_Y
OnScreenController.Button.DPAD_UP -> KeyEvent.KEYCODE_DPAD_UP
OnScreenController.Button.DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
OnScreenController.Button.DPAD_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
OnScreenController.Button.DPAD_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
OnScreenController.Button.LEFT_BUMPER -> KeyEvent.KEYCODE_BUTTON_L1
OnScreenController.Button.RIGHT_BUMPER -> KeyEvent.KEYCODE_BUTTON_R1
OnScreenController.Button.LEFT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_L2
OnScreenController.Button.RIGHT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_R2
OnScreenController.Button.START -> KeyEvent.KEYCODE_BUTTON_START
OnScreenController.Button.BACK -> KeyEvent.KEYCODE_BUTTON_SELECT
OnScreenController.Button.LEFT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBL
OnScreenController.Button.RIGHT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBR
OnScreenController.Button.BLACK -> KeyEvent.KEYCODE_BUTTON_L2
OnScreenController.Button.WHITE -> KeyEvent.KEYCODE_BUTTON_R2
}
}
}
@@ -0,0 +1,34 @@
package com.izzy2lost.x1box
import android.content.Context
import android.content.SharedPreferences
class ControllerSettings(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(
"controller_settings",
Context.MODE_PRIVATE
)
companion object {
private const val KEY_SHOW_ON_SCREEN_CONTROLLER = "show_on_screen_controller"
private const val KEY_CONTROLLER_OPACITY = "controller_opacity"
private const val KEY_CONTROLLER_SCALE = "controller_scale"
private const val KEY_VIBRATION_ENABLED = "vibration_enabled"
}
var showOnScreenController: Boolean
get() = prefs.getBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, true)
set(value) = prefs.edit().putBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, value).apply()
var controllerOpacity: Float
get() = prefs.getFloat(KEY_CONTROLLER_OPACITY, 0.7f)
set(value) = prefs.edit().putFloat(KEY_CONTROLLER_OPACITY, value.coerceIn(0f, 1f)).apply()
var controllerScale: Float
get() = prefs.getFloat(KEY_CONTROLLER_SCALE, 1.0f)
set(value) = prefs.edit().putFloat(KEY_CONTROLLER_SCALE, value.coerceIn(0.5f, 2.0f)).apply()
var vibrationEnabled: Boolean
get() = prefs.getBoolean(KEY_VIBRATION_ENABLED, true)
set(value) = prefs.edit().putBoolean(KEY_VIBRATION_ENABLED, value).apply()
}
@@ -1,6 +1,7 @@
package com.izzy2lost.x1box
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
@@ -208,10 +209,13 @@ class GameLibraryActivity : AppCompatActivity() {
private fun renderCoverGrid(games: List<GameEntry>) {
val inflater = LayoutInflater.from(this)
var row: LinearLayout? = null
val columns = resolveCoverGridColumns()
val spacingPx = dp(8)
val halfSpacingPx = spacingPx / 2
for ((index, game) in games.withIndex()) {
if (index % 2 == 0) {
val columnIndex = index % columns
if (columnIndex == 0) {
row = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
}
@@ -219,7 +223,7 @@ class GameLibraryActivity : AppCompatActivity() {
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
if (index > 0) {
if (index >= columns) {
rowLp.topMargin = dp(12)
}
gamesGridContainer.addView(row, rowLp)
@@ -227,11 +231,8 @@ class GameLibraryActivity : AppCompatActivity() {
val item = inflater.inflate(R.layout.item_game_cover, row, false)
val itemLp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
if (index % 2 == 0) {
itemLp.marginEnd = spacingPx
} else {
itemLp.marginStart = spacingPx
}
itemLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx
itemLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx
row!!.addView(item, itemLp)
val nameText = item.findViewById<TextView>(R.id.game_cover_name_text)
@@ -244,11 +245,25 @@ class GameLibraryActivity : AppCompatActivity() {
bindCoverArt(coverImage, game)
}
if (games.size % 2 != 0) {
val filler = Space(this)
val fillerLp = LinearLayout.LayoutParams(0, 0, 1f)
fillerLp.marginStart = spacingPx
row?.addView(filler, fillerLp)
val remainder = games.size % columns
if (remainder != 0) {
for (columnIndex in remainder until columns) {
val filler = Space(this)
val fillerLp = LinearLayout.LayoutParams(0, 0, 1f)
fillerLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx
fillerLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx
row?.addView(filler, fillerLp)
}
}
}
private fun resolveCoverGridColumns(): Int {
val widthDp = resources.configuration.screenWidthDp
val suggested = (widthDp / 180).coerceIn(2, 4)
return if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
maxOf(3, suggested)
} else {
suggested
}
}
@@ -1,11 +1,198 @@
package com.izzy2lost.x1box
import android.content.Context
import android.hardware.input.InputManager
import android.os.Build
import android.os.Bundle
import android.view.InputDevice
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.widget.FrameLayout
import org.libsdl.app.SDLActivity
class MainActivity : SDLActivity() {
class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private var onScreenController: OnScreenController? = null
private var controllerBridge: ControllerInputBridge? = null
private var isControllerVisible = false
private var inputManager: InputManager? = null
private var hasPhysicalController = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupOnScreenController()
setupControllerDetection()
hideSystemUI()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideSystemUI()
}
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 (API 30) and above
window.setDecorFitsSystemWindows(false)
window.insetsController?.let { controller ->
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
// Android 10 and below
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
)
}
}
private fun setupOnScreenController() {
// Create on-screen controller
onScreenController = OnScreenController(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
// Create input bridge
controllerBridge = ControllerInputBridge()
onScreenController?.setControllerListener(controllerBridge!!)
// Add to layout
mLayout?.addView(onScreenController)
// Check for existing controllers and show/hide accordingly
updateControllerVisibility()
}
override fun onResume() {
super.onResume()
// Register virtual controller after SDL is initialized
// Use a delay to ensure SDL is fully ready
mLayout?.postDelayed({
registerVirtualController()
}, 1000)
}
private fun registerVirtualController() {
try {
// Register the virtual on-screen controller as a joystick device
// Device ID: -2, Name: "On-Screen Controller"
org.libsdl.app.SDLControllerManager.nativeAddJoystick(
-2, // device_id
"On-Screen Controller", // name
"Virtual touchscreen controller", // desc
0x045e, // vendor_id (Microsoft)
0x028e, // product_id (Xbox 360 Controller)
false, // is_accelerometer
0xFFFF, // button_mask (all buttons)
6, // naxes (left X/Y, right X/Y, left trigger, right trigger)
0x3F, // axis_mask (6 axes)
0, // nhats
0 // nballs
)
android.util.Log.d("MainActivity", "Virtual controller registered successfully")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to register virtual controller: ${e.message}")
}
}
private fun setupControllerDetection() {
inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager
inputManager?.registerInputDeviceListener(this, null)
// Check for already connected controllers
checkForPhysicalControllers()
}
private fun checkForPhysicalControllers() {
val deviceIds = inputManager?.inputDeviceIds ?: return
hasPhysicalController = deviceIds.any { deviceId ->
val device = inputManager?.getInputDevice(deviceId)
isGameController(device)
}
updateControllerVisibility()
}
private fun isGameController(device: InputDevice?): Boolean {
if (device == null) return false
val sources = device.sources
// Check if device is a gamepad or joystick
return ((sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)
}
private fun updateControllerVisibility() {
// Show on-screen controller only if no physical controller is connected
val shouldShow = !hasPhysicalController
if (shouldShow != isControllerVisible) {
isControllerVisible = shouldShow
onScreenController?.visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
// InputDeviceListener callbacks
override fun onInputDeviceAdded(deviceId: Int) {
val device = inputManager?.getInputDevice(deviceId)
if (isGameController(device)) {
hasPhysicalController = true
updateControllerVisibility()
}
}
override fun onInputDeviceRemoved(deviceId: Int) {
// Recheck all devices to see if any controllers remain
checkForPhysicalControllers()
}
override fun onInputDeviceChanged(deviceId: Int) {
// Recheck all devices in case configuration changed
checkForPhysicalControllers()
}
override fun onDestroy() {
// Unregister virtual controller
try {
org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to unregister virtual controller: ${e.message}")
}
inputManager?.unregisterInputDeviceListener(this)
super.onDestroy()
}
// Manual control methods (for settings/preferences)
fun toggleOnScreenController() {
isControllerVisible = !isControllerVisible
onScreenController?.visibility = if (isControllerVisible) View.VISIBLE else View.GONE
}
fun showOnScreenController() {
isControllerVisible = true
onScreenController?.visibility = View.VISIBLE
}
fun hideOnScreenController() {
isControllerVisible = false
onScreenController?.visibility = View.GONE
}
fun forceUpdateControllerVisibility() {
checkForPhysicalControllers()
}
override fun getLibraries(): Array<String> = arrayOf(
@@ -0,0 +1,407 @@
package com.izzy2lost.x1box
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.pow
import kotlin.math.sqrt
class OnScreenController @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val buttons = mutableMapOf<Button, ButtonState>()
private val sticks = mutableMapOf<Stick, StickState>()
private var controllerListener: ControllerListener? = null
enum class Button {
A, B, X, Y,
DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT,
LEFT_TRIGGER, RIGHT_TRIGGER,
LEFT_BUMPER, RIGHT_BUMPER,
START, BACK,
LEFT_STICK_BUTTON, RIGHT_STICK_BUTTON,
BLACK, WHITE
}
enum class Stick {
LEFT, RIGHT
}
data class ButtonState(
val center: PointF,
val radius: Float,
var isPressed: Boolean = false
)
data class StickState(
val center: PointF,
val radius: Float,
val deadZone: Float = 0.2f,
var currentPos: PointF = PointF(0f, 0f),
var isPressed: Boolean = false,
var activePointerId: Int = -1
)
interface ControllerListener {
fun onButtonPressed(button: Button)
fun onButtonReleased(button: Button)
fun onStickMoved(stick: Stick, x: Float, y: Float)
fun onStickPressed(stick: Stick)
fun onStickReleased(stick: Stick)
}
init {
setBackgroundColor(Color.TRANSPARENT)
}
fun setControllerListener(listener: ControllerListener) {
this.controllerListener = listener
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
initializeControls(w, h)
}
private fun initializeControls(width: Int, height: Int) {
val w = width.toFloat()
val h = height.toFloat()
// Button sizes - made D-pad smaller
val faceButtonRadius = w * 0.035f
val dpadButtonRadius = w * 0.025f
val shoulderButtonRadius = w * 0.042f
val smallButtonRadius = w * 0.022f
val stickRadius = w * 0.07f
// Face buttons (right side) - A, B, X, Y in diamond formation
val faceButtonCenterX = w * 0.88f
val faceButtonCenterY = h * 0.55f
val faceButtonSpacing = w * 0.07f
buttons[Button.A] = ButtonState(
PointF(faceButtonCenterX, faceButtonCenterY + faceButtonSpacing),
faceButtonRadius
)
buttons[Button.B] = ButtonState(
PointF(faceButtonCenterX + faceButtonSpacing, faceButtonCenterY),
faceButtonRadius
)
buttons[Button.X] = ButtonState(
PointF(faceButtonCenterX - faceButtonSpacing, faceButtonCenterY),
faceButtonRadius
)
buttons[Button.Y] = ButtonState(
PointF(faceButtonCenterX, faceButtonCenterY - faceButtonSpacing),
faceButtonRadius
)
// D-Pad (bottom left corner) - smaller buttons
val dpadCenterX = w * 0.12f
val dpadCenterY = h * 0.85f
val dpadSpacing = w * 0.045f
buttons[Button.DPAD_UP] = ButtonState(
PointF(dpadCenterX, dpadCenterY - dpadSpacing),
dpadButtonRadius
)
buttons[Button.DPAD_DOWN] = ButtonState(
PointF(dpadCenterX, dpadCenterY + dpadSpacing),
dpadButtonRadius
)
buttons[Button.DPAD_LEFT] = ButtonState(
PointF(dpadCenterX - dpadSpacing, dpadCenterY),
dpadButtonRadius
)
buttons[Button.DPAD_RIGHT] = ButtonState(
PointF(dpadCenterX + dpadSpacing, dpadCenterY),
dpadButtonRadius
)
// Shoulder buttons (triggers and bumpers)
buttons[Button.LEFT_TRIGGER] = ButtonState(
PointF(w * 0.12f, h * 0.08f),
shoulderButtonRadius
)
buttons[Button.RIGHT_TRIGGER] = ButtonState(
PointF(w * 0.88f, h * 0.08f),
shoulderButtonRadius
)
buttons[Button.LEFT_BUMPER] = ButtonState(
PointF(w * 0.12f, h * 0.16f),
shoulderButtonRadius
)
buttons[Button.RIGHT_BUMPER] = ButtonState(
PointF(w * 0.88f, h * 0.16f),
shoulderButtonRadius
)
// Center buttons
buttons[Button.START] = ButtonState(
PointF(w * 0.58f, h * 0.4f),
smallButtonRadius
)
buttons[Button.BACK] = ButtonState(
PointF(w * 0.42f, h * 0.4f),
smallButtonRadius
)
// Analog sticks - right stick moved down
sticks[Stick.LEFT] = StickState(
PointF(w * 0.18f, h * 0.45f),
stickRadius
)
sticks[Stick.RIGHT] = StickState(
PointF(w * 0.62f, h * 0.82f),
stickRadius
)
// Black and White buttons - moved to right of right analog stick
buttons[Button.WHITE] = ButtonState(
PointF(w * 0.75f, h * 0.78f),
smallButtonRadius
)
buttons[Button.BLACK] = ButtonState(
PointF(w * 0.75f, h * 0.86f),
smallButtonRadius
)
// Stick buttons
buttons[Button.LEFT_STICK_BUTTON] = ButtonState(
sticks[Stick.LEFT]!!.center,
stickRadius * 0.3f
)
buttons[Button.RIGHT_STICK_BUTTON] = ButtonState(
sticks[Stick.RIGHT]!!.center,
stickRadius * 0.3f
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Draw analog sticks
sticks.forEach { (stick, state) ->
// Outer circle
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
paint.color = Color.argb(100, 255, 255, 255)
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
// Dead zone circle
paint.color = Color.argb(50, 255, 255, 255)
canvas.drawCircle(state.center.x, state.center.y, state.radius * state.deadZone, paint)
// Stick position
val stickX = state.center.x + state.currentPos.x * state.radius
val stickY = state.center.y + state.currentPos.y * state.radius
paint.style = Paint.Style.FILL
paint.color = if (state.isPressed) {
Color.argb(200, 100, 150, 255)
} else {
Color.argb(150, 200, 200, 200)
}
canvas.drawCircle(stickX, stickY, state.radius * 0.4f, paint)
}
// Draw buttons
buttons.forEach { (button, state) ->
// Skip stick buttons as they're drawn with sticks
if (button == Button.LEFT_STICK_BUTTON || button == Button.RIGHT_STICK_BUTTON) {
return@forEach
}
paint.style = Paint.Style.FILL
paint.color = when {
state.isPressed -> getButtonPressedColor(button)
else -> getButtonColor(button)
}
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
// Button outline
paint.style = Paint.Style.STROKE
paint.strokeWidth = 3f
paint.color = Color.argb(150, 255, 255, 255)
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
// Button labels
paint.style = Paint.Style.FILL
paint.color = Color.WHITE
paint.textSize = state.radius * 0.8f
paint.textAlign = Paint.Align.CENTER
val label = getButtonLabel(button)
canvas.drawText(label, state.center.x, state.center.y + state.radius * 0.3f, paint)
}
}
private fun getButtonColor(button: Button): Int {
return when (button) {
Button.A -> Color.argb(150, 100, 200, 100)
Button.B -> Color.argb(150, 200, 100, 100)
Button.X -> Color.argb(150, 100, 150, 255)
Button.Y -> Color.argb(150, 255, 255, 100)
Button.BLACK -> Color.argb(150, 50, 50, 50)
Button.WHITE -> Color.argb(150, 220, 220, 220)
else -> Color.argb(120, 150, 150, 150)
}
}
private fun getButtonPressedColor(button: Button): Int {
return when (button) {
Button.A -> Color.argb(255, 100, 255, 100)
Button.B -> Color.argb(255, 255, 100, 100)
Button.X -> Color.argb(255, 100, 150, 255)
Button.Y -> Color.argb(255, 255, 255, 100)
Button.BLACK -> Color.argb(255, 80, 80, 80)
Button.WHITE -> Color.argb(255, 255, 255, 255)
else -> Color.argb(200, 200, 200, 200)
}
}
private fun getButtonLabel(button: Button): String {
return when (button) {
Button.A -> "A"
Button.B -> "B"
Button.X -> "X"
Button.Y -> "Y"
Button.DPAD_UP -> ""
Button.DPAD_DOWN -> ""
Button.DPAD_LEFT -> ""
Button.DPAD_RIGHT -> ""
Button.LEFT_TRIGGER -> "LT"
Button.RIGHT_TRIGGER -> "RT"
Button.LEFT_BUMPER -> "LB"
Button.RIGHT_BUMPER -> "RB"
Button.START -> ""
Button.BACK -> ""
Button.BLACK -> "BK"
Button.WHITE -> "WH"
else -> ""
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
handleTouchDown(x, y, pointerId)
}
MotionEvent.ACTION_MOVE -> {
for (i in 0 until event.pointerCount) {
handleTouchMove(
event.getX(i),
event.getY(i),
event.getPointerId(i)
)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
handleTouchUp(pointerId)
}
}
invalidate()
return true
}
private fun handleTouchDown(x: Float, y: Float, pointerId: Int) {
// Check sticks first
sticks.forEach { (stick, state) ->
if (state.activePointerId == -1 && isPointInCircle(x, y, state.center, state.radius)) {
state.activePointerId = pointerId
updateStickPosition(stick, state, x, y)
return
}
}
// Check buttons
buttons.forEach { (button, state) ->
if (isPointInCircle(x, y, state.center, state.radius)) {
state.isPressed = true
controllerListener?.onButtonPressed(button)
return
}
}
}
private fun handleTouchMove(x: Float, y: Float, pointerId: Int) {
sticks.forEach { (stick, state) ->
if (state.activePointerId == pointerId) {
updateStickPosition(stick, state, x, y)
}
}
}
private fun handleTouchUp(pointerId: Int) {
// Release sticks
sticks.forEach { (stick, state) ->
if (state.activePointerId == pointerId) {
state.activePointerId = -1
state.currentPos = PointF(0f, 0f)
controllerListener?.onStickMoved(stick, 0f, 0f)
if (state.isPressed) {
state.isPressed = false
controllerListener?.onStickReleased(stick)
}
}
}
// Release buttons
buttons.forEach { (button, state) ->
if (state.isPressed) {
state.isPressed = false
controllerListener?.onButtonReleased(button)
}
}
}
private fun updateStickPosition(stick: Stick, state: StickState, x: Float, y: Float) {
val dx = x - state.center.x
val dy = y - state.center.y
val distance = sqrt(dx.pow(2) + dy.pow(2))
if (distance > state.radius) {
// Clamp to circle boundary
state.currentPos.x = (dx / distance)
state.currentPos.y = (dy / distance)
} else {
// Normalize to -1..1 range
state.currentPos.x = dx / state.radius
state.currentPos.y = dy / state.radius
}
// Apply dead zone
val magnitude = sqrt(state.currentPos.x.pow(2) + state.currentPos.y.pow(2))
if (magnitude < state.deadZone) {
state.currentPos.x = 0f
state.currentPos.y = 0f
}
controllerListener?.onStickMoved(stick, state.currentPos.x, state.currentPos.y)
}
private fun isPointInCircle(x: Float, y: Float, center: PointF, radius: Float): Boolean {
val dx = x - center.x
val dy = y - center.y
return sqrt(dx.pow(2) + dy.pow(2)) <= radius
}
fun setVisibility(visible: Boolean) {
visibility = if (visible) View.VISIBLE else View.GONE
}
}
@@ -12,38 +12,48 @@
app:strokeColor="@color/xemu_outline"
app:strokeWidth="1dp">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<ImageView
android:id="@+id/game_cover_image"
android:layout_width="match_parent"
android:layout_height="190dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/setup_wizard_path_background"
android:contentDescription="@string/library_open_game"
android:scaleType="centerCrop"
android:src="@android:drawable/ic_menu_report_image" />
android:scaleType="fitCenter"
android:src="@android:drawable/ic_menu_report_image"
app:layout_constraintDimensionRatio="2:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/game_cover_name_text"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" />
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/game_cover_image" />
<TextView
android:id="@+id/game_cover_size_text"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
</LinearLayout>
android:textColor="@color/xemu_text_muted"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/game_cover_name_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
+7 -1
View File
@@ -290,4 +290,10 @@ static void usb_xid_register_types(void)
type_register_static(&usb_xbox_gamepad_s_info);
}
type_init(usb_xid_register_types)
type_init(usb_xid_register_types)
#ifdef __ANDROID__
void xemu_android_force_xid_gamepad_link(void)
{
}
#endif
+22 -4
View File
@@ -167,6 +167,10 @@ static void check_and_reset_in_range(int *btn, int min, int max,
static void xemu_input_bindings_set_in_range(ControllerState *con)
{
if (!con->controller_map) {
return;
}
#define CHECK_RESET_BUTTON(btn) \
check_and_reset_in_range(&con->controller_map->controller_mapping.btn, \
SDL_CONTROLLER_BUTTON_INVALID, \
@@ -213,7 +217,14 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
char guid[35] = { 0 };
SDL_JoystickGetGUIDString(con->sdl_joystick_guid, guid, sizeof(guid));
if (!xemu_settings_load_gamepad_mapping(guid, &con->controller_map)) {
bool added_mapping =
xemu_settings_load_gamepad_mapping(guid, &con->controller_map);
if (!con->controller_map) {
fprintf(stderr, "Failed to load gamepad mapping for %s\n", guid);
return;
}
if (!added_mapping) {
return;
}
@@ -221,7 +232,7 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
// have been reallocated. Any gamepad mapping pointers for other controllers
// are now invalid, and need to be reloaded.
ControllerState *iter, *next;
bool is_new_mapping;
bool iter_is_new_mapping;
QTAILQ_FOREACH_SAFE (iter, &available_controllers, entry, next) {
if (iter == con || iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) {
continue;
@@ -230,9 +241,9 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
memset(guid, 0, sizeof(guid));
SDL_JoystickGetGUIDString(iter->sdl_joystick_guid, guid, sizeof(guid));
is_new_mapping =
iter_is_new_mapping =
xemu_settings_load_gamepad_mapping(guid, &iter->controller_map);
assert(!is_new_mapping &&
assert(!iter_is_new_mapping &&
"Existing controller GUIDs should exist in the config");
xemu_input_bindings_set_in_range(iter);
@@ -566,6 +577,9 @@ void xemu_input_update_sdl_controller_state(ControllerState *state)
{
state->buttons = 0;
memset(state->axis, 0, sizeof(state->axis));
if (!state->controller_map) {
return;
}
#define SDL_MASK_BUTTON(state, btn, idx) \
(SDL_GameControllerGetButton( \
@@ -636,6 +650,10 @@ void xemu_input_update_rumble(ControllerState *state)
return;
}
if (!state->controller_map) {
return;
}
if (!state->controller_map->enable_rumble) {
return;
}