diff --git a/.gitignore b/.gitignore index 8faf6026a8..121fd33ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ javadoc_deploy.pub !.vscode/settings.json !.vscode/JME_style.xml !.vscode/extensions.json +joysticks-*.txt \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b19a4b767..1244e46c51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ lwjgl3-jemalloc = { module = "org.lwjgl:lwjgl-jemalloc", version.ref = "lwjgl3" lwjgl3-openal = { module = "org.lwjgl:lwjgl-openal", version.ref = "lwjgl3" } lwjgl3-opencl = { module = "org.lwjgl:lwjgl-opencl", version.ref = "lwjgl3" } lwjgl3-opengl = { module = "org.lwjgl:lwjgl-opengl", version.ref = "lwjgl3" } +lwjgl3-sdl = { module = "org.lwjgl:lwjgl-sdl", version.ref = "lwjgl3" } mokito-core = "org.mockito:mockito-core:3.12.4" diff --git a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java index 212f500e3b..ceb19bc280 100644 --- a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java +++ b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java @@ -48,6 +48,7 @@ public class DefaultJoystickAxis implements JoystickAxis { private final boolean isAnalog; private final boolean isRelative; private float deadZone; + private float jitterThreshold = 0f; /** * Creates a new joystick axis instance. Only used internally. @@ -166,6 +167,12 @@ public void setDeadZone(float f) { public String toString() { return "JoystickAxis[name=" + name + ", parent=" + parent.getName() + ", id=" + axisIndex + ", logicalId=" + logicalId + ", isAnalog=" + isAnalog - + ", isRelative=" + isRelative + ", deadZone=" + deadZone + "]"; + + ", isRelative=" + isRelative + ", deadZone=" + deadZone + + ", jitterThreshold=" + jitterThreshold + "]"; + } + + @Override + public float getJitterThreshold() { + return jitterThreshold; } } diff --git a/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java b/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java index fbbddb9f06..4168e32711 100644 --- a/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java +++ b/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java @@ -44,27 +44,14 @@ public interface JoystickAxis { public static final String Z_ROTATION = "rz"; public static final String LEFT_TRIGGER = "rx"; public static final String RIGHT_TRIGGER = "ry"; - - // Note: the left/right trigger bit may be a bit controversial in - // the sense that this is one case where XBox controllers make a lot - // more sense. - // I've seen the following mappings for various things: - // - // Axis | XBox | Non-Xbox (generally) (includes actual Sony PS4 controllers) - // --------------+-------+--------------- - // left trigger | z | rx (also button 6) - // right trigger | rz | ry (also button 7) - // left stick x | x | x - // left stick y | y | y - // right stick x | rx | z - // right stick y | ry | rz - // - // The issue is that in all cases I've seen, the XBox controllers will - // use the name "xbox" somewhere in their name. The Non-XBox controllers - // never mention anything uniform... even the PS4 controller only calls - // itself "Wireless Controller". In that light, it seems easier to make - // the default the ugly case and the "XBox" way the exception because it - // can more easily be identified. + + public static final String AXIS_XBOX_LEFT_TRIGGER = LEFT_TRIGGER; + public static final String AXIS_XBOX_RIGHT_TRIGGER = RIGHT_TRIGGER; + public static final String AXIS_XBOX_LEFT_THUMB_STICK_X = X_AXIS; + public static final String AXIS_XBOX_LEFT_THUMB_STICK_Y = Y_AXIS; + public static final String AXIS_XBOX_RIGHT_THUMB_STICK_X = Z_AXIS; + public static final String AXIS_XBOX_RIGHT_THUMB_STICK_Y = Z_ROTATION; + public static final String POV_X = "pov_x"; public static final String POV_Y = "pov_y"; @@ -128,4 +115,15 @@ public interface JoystickAxis { * @return the radius of the dead zone */ public float getDeadZone(); + + + /** + * Returns the suggested jitter threshold for this axis. Movements with a delta + * smaller than this threshold will be ignored by the backend input system + * + * @return the jitter threshold + */ + public default float getJitterThreshold(){ + return 0; + } } diff --git a/jme3-core/src/main/java/com/jme3/input/JoystickButton.java b/jme3-core/src/main/java/com/jme3/input/JoystickButton.java index 8d2336c18c..11c0155087 100644 --- a/jme3-core/src/main/java/com/jme3/input/JoystickButton.java +++ b/jme3-core/src/main/java/com/jme3/input/JoystickButton.java @@ -55,6 +55,26 @@ public interface JoystickButton { public static final String BUTTON_14 = "14"; public static final String BUTTON_15 = "15"; + + public static final String BUTTON_XBOX_A = BUTTON_2; + public static final String BUTTON_XBOX_B = BUTTON_1; + public static final String BUTTON_XBOX_X = BUTTON_3; + public static final String BUTTON_XBOX_Y = BUTTON_0; + public static final String BUTTON_XBOX_LB = BUTTON_4; + public static final String BUTTON_XBOX_RB = BUTTON_5; + public static final String BUTTON_XBOX_LT = BUTTON_6; + public static final String BUTTON_XBOX_RT = BUTTON_7; + public static final String BUTTON_XBOX_BACK = BUTTON_8; + public static final String BUTTON_XBOX_START = BUTTON_9; + public static final String BUTTON_XBOX_L3 = BUTTON_10; + public static final String BUTTON_XBOX_R3 = BUTTON_11; + + public static final String BUTTON_XBOX_DPAD_UP = BUTTON_12; + public static final String BUTTON_XBOX_DPAD_DOWN = BUTTON_13; + public static final String BUTTON_XBOX_DPAD_LEFT = BUTTON_14; + public static final String BUTTON_XBOX_DPAD_RIGHT = BUTTON_15; + + /** * Assign the mapping name to receive events from the given button index * on the joystick. diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index e159586f2f..92354ff91e 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -268,6 +268,31 @@ public final class AppSettings extends HashMap { */ public static final String JOAL = "JOAL"; + /** + * Map gamepads to Xbox-like layout. + */ + public static final String JOYSTICKS_XBOX_MAPPER = "JOYSTICKS_XBOX_MAPPER"; + + /** + * Map gamepads to an Xbox-like layout, with fallback to raw if the gamepad is not recognized. + */ + public static final String JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER = "JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER"; + + /** + * Map gamepads to an Xbox-like layout using the legacy jME input + */ + public static final String JOYSTICKS_XBOX_LEGACY_MAPPER = "JOYSTICKS_XBOX_LEGACY_MAPPER"; + + /** + * Map gamepads using the legacy jME mapper and input. + */ + public static final String JOYSTICKS_LEGACY_MAPPER = "JOYSTICKS_LEGACY_MAPPER"; + + /** + * Don't map gamepads, use raw events instead (ie. bring your own mapper) + */ + public static final String JOYSTICKS_RAW_MAPPER = "JOYSTICKS_RAW_MAPPER"; + static { defaults.put("Display", 0); defaults.put("CenterWindow", true); @@ -300,6 +325,10 @@ public final class AppSettings extends HashMap { defaults.put("WindowYPosition", 0); defaults.put("WindowXPosition", 0); defaults.put("X11PlatformPreferred", false); + defaults.put("JoysticksMapper", JOYSTICKS_XBOX_MAPPER); + defaults.put("JoysticksTriggerToButtonThreshold", 0.5f); + defaults.put("JoysticksAxisJitterThreshold", 0.0001f); + defaults.put("SDLGameControllerDBResourcePath", ""); // defaults.put("Icons", null); } @@ -1612,4 +1641,85 @@ public void setX11PlatformPreferred(boolean preferred) { public boolean isX11PlatformPreferred() { return getBoolean("X11PlatformPreferred"); } + + /** + * Set which joystick mapping to use for normalization of controller inputs + * + * @param mapper + * JOYSTICKS_MAPPER_* constant defining which mapping to use + */ + public void setJoysticksMapper(String mapper) { + putString("JoysticksMapper", mapper); + } + + /** + * Get which joystick mapping to use for normalization of controller inputs + */ + public String getJoysticksMapper() { + return getString("JoysticksMapper"); + } + + /** + * Sets the threshold above which an analog trigger should also generate a button-press event. + * If the value is set to -1, the trigger will never generate button-press events. + * + *

+ * This is intended to normalize behavior between controllers that expose triggers as analog + * axes and controllers that expose triggers as digital buttons. + * + * @param threshold the trigger threshold in the range [0, 1] (default: 0.5f) + */ + public void setJoysticksTriggerToButtonThreshold(float threshold) { + putFloat("JoysticksTriggerToButtonThreshold", threshold); + } + + /** + * Gets the threshold above which an analog trigger should also generate a button-press event. + * + * @return the trigger threshold in the range [0, 1] (default: 0.5f) + * @see #setJoysticksTriggerToButtonThreshold(float) + */ + public float getJoysticksTriggerToButtonThreshold() { + return getFloat("JoysticksTriggerToButtonThreshold"); + } + + /** + * Sets the jitter threshold for joystick axes. + * + *

+ * Axis movements with a delta smaller than this threshold will be ignored. This is intended to reduce + * noise from analog joysticks. + */ + public void setJoysticksAxisJitterThreshold(float threshold) { + putFloat("JoysticksAxisJitterThreshold", threshold); + } + + /** + * Gets the jitter threshold for joystick axes. + * + * @return the jitter threshold + * @see #setJoysticksAxisJitterThreshold(float) + */ + public float getJoysticksAxisJitterThreshold() { + return getFloat("JoysticksAxisJitterThreshold"); + } + + /** + * Set resource path for a custom SDL game controller database. + * + * @param path + */ + public void setSDLGameControllerDBResourcePath(String path) { + putString("SDLGameControllerDBResourcePath", path); + } + + /** + * Get resource path for a custom SDL game controller database. + * + * @return resource path + */ + public String getSDLGameControllerDBResourcePath() { + return getString("SDLGameControllerDBResourcePath"); + } } + diff --git a/jme3-examples/build.gradle b/jme3-examples/build.gradle index 4f5d41b501..e3a5048fc1 100644 --- a/jme3-examples/build.gradle +++ b/jme3-examples/build.gradle @@ -19,8 +19,8 @@ dependencies { implementation project(':jme3-effects') implementation project(':jme3-jbullet') implementation project(':jme3-jogg') - implementation project(':jme3-lwjgl') -// implementation project(':jme3-lwjgl3') + // implementation project(':jme3-lwjgl') + implementation project(':jme3-lwjgl3') implementation project(':jme3-networking') implementation project(':jme3-niftygui') implementation project(':jme3-plugins') diff --git a/jme3-examples/src/main/java/jme3test/input/TestJoystick.java b/jme3-examples/src/main/java/jme3test/input/TestJoystick.java index 3f25532d50..6ef8435d98 100644 --- a/jme3-examples/src/main/java/jme3test/input/TestJoystick.java +++ b/jme3-examples/src/main/java/jme3test/input/TestJoystick.java @@ -46,7 +46,9 @@ public class TestJoystick extends SimpleApplication { public static void main(String[] args){ TestJoystick app = new TestJoystick(); AppSettings settings = new AppSettings(true); + settings.setJoysticksMapper(AppSettings.JOYSTICKS_XBOX_MAPPER); settings.setUseJoysticks(true); + settings.setX11PlatformPreferred(true); app.setSettings(settings); app.start(); } @@ -155,7 +157,7 @@ protected void setViewedJoystick( Joystick stick ) { } } - + /** * Easier to watch for all button and axis events with a raw input listener. */ @@ -184,6 +186,7 @@ public void onJoyAxisEvent(JoyAxisEvent evt) { gamepad.setAxisValue( evt.getAxis(), value ); if( value != 0 ) { lastValues.put(evt.getAxis(), value); + evt.getAxis().getJoystick().rumble(0.5f); } } @@ -191,6 +194,7 @@ public void onJoyAxisEvent(JoyAxisEvent evt) { public void onJoyButtonEvent(JoyButtonEvent evt) { setViewedJoystick( evt.getButton().getJoystick() ); gamepad.setButtonValue( evt.getButton(), evt.isPressed() ); + evt.getButton().getJoystick().rumble(1f); } @Override @@ -255,24 +259,24 @@ public GamepadView() { attachChild(rightStick); // A "standard" mapping... fits a majority of my game pads - addButton( JoystickButton.BUTTON_0, 371, 512 - 176, 42, 42 ); - addButton( JoystickButton.BUTTON_1, 407, 512 - 212, 42, 42 ); - addButton( JoystickButton.BUTTON_2, 371, 512 - 248, 42, 42 ); - addButton( JoystickButton.BUTTON_3, 334, 512 - 212, 42, 42 ); + addButton( JoystickButton.BUTTON_XBOX_Y, 371, 512 - 176, 42, 42 ); + addButton( JoystickButton.BUTTON_XBOX_B, 407, 512 - 212, 42, 42 ); + addButton( JoystickButton.BUTTON_XBOX_A, 371, 512 - 248, 42, 42 ); + addButton( JoystickButton.BUTTON_XBOX_X, 334, 512 - 212, 42, 42 ); // Front buttons Some of these have the top ones and the bottoms ones flipped. - addButton( JoystickButton.BUTTON_4, 67, 512 - 111, 95, 21 ); - addButton( JoystickButton.BUTTON_5, 348, 512 - 111, 95, 21 ); - addButton( JoystickButton.BUTTON_6, 67, 512 - 89, 95, 21 ); - addButton( JoystickButton.BUTTON_7, 348, 512 - 89, 95, 21 ); + addButton( JoystickButton.BUTTON_XBOX_LB, 67, 512 - 111, 95, 21 ); + addButton( JoystickButton.BUTTON_XBOX_RB, 348, 512 - 111, 95, 21 ); + addButton( JoystickButton.BUTTON_XBOX_LT, 67, 512 - 89, 95, 21 ); + addButton( JoystickButton.BUTTON_XBOX_RT, 348, 512 - 89, 95, 21 ); // Select and start buttons - addButton( JoystickButton.BUTTON_8, 206, 512 - 198, 48, 30 ); - addButton( JoystickButton.BUTTON_9, 262, 512 - 198, 48, 30 ); + addButton( JoystickButton.BUTTON_XBOX_BACK, 206, 512 - 198, 48, 30 ); + addButton( JoystickButton.BUTTON_XBOX_START, 262, 512 - 198, 48, 30 ); // Joystick push buttons - addButton( JoystickButton.BUTTON_10, 147, 512 - 300, 75, 70 ); - addButton( JoystickButton.BUTTON_11, 285, 512 - 300, 75, 70 ); + addButton( JoystickButton.BUTTON_XBOX_L3, 147, 512 - 300, 75, 70 ); + addButton( JoystickButton.BUTTON_XBOX_R3, 285, 512 - 300, 75, 70 ); // Fake button highlights for the POV axes // @@ -280,10 +284,14 @@ public GamepadView() { // -X +X // -Y // - addButton( "POV +Y", 96, 512 - 174, 40, 38 ); - addButton( "POV +X", 128, 512 - 208, 40, 38 ); - addButton( "POV -Y", 96, 512 - 239, 40, 38 ); - addButton( "POV -X", 65, 512 - 208, 40, 38 ); + // addButton( "POV +Y", 96, 512 - 174, 40, 38 ); + // addButton( "POV +X", 128, 512 - 208, 40, 38 ); + // addButton( "POV -Y", 96, 512 - 239, 40, 38 ); + // addButton( "POV -X", 65, 512 - 208, 40, 38 ); + addButton( JoystickButton.BUTTON_XBOX_DPAD_UP, 96, 512 - 174, 40, 38 ); + addButton( JoystickButton.BUTTON_XBOX_DPAD_RIGHT, 128, 512 - 208, 40, 38 ); + addButton( JoystickButton.BUTTON_XBOX_DPAD_DOWN, 96, 512 - 239, 40, 38 ); + addButton( JoystickButton.BUTTON_XBOX_DPAD_LEFT, 65, 512 - 208, 40, 38 ); resetPositions(); } @@ -295,22 +303,21 @@ private void addButton( String name, float x, float y, float width, float height } public void setAxisValue( JoystickAxis axis, float value ) { - - System.out.println( "Axis:" + axis.getName() + "(id:" + axis.getLogicalId() + ")=" + value ); - if( axis == axis.getJoystick().getXAxis() ) { + + if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X)){ setXAxis(value); - } else if( axis == axis.getJoystick().getYAxis() ) { + } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y)){ setYAxis(-value); - } else if( axis == axis.getJoystick().getAxis(JoystickAxis.Z_AXIS) ) { + } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X)) { // Note: in the above condition, we could check the axis name, but // I have at least one joystick that reports 2 "Z Axis" axes. // In this particular case, the first one is the right one so // a name based lookup will find the proper one. It's a problem // because the erroneous axis sends a constant stream of values. setZAxis(value); - } else if( axis == axis.getJoystick().getAxis(JoystickAxis.Z_ROTATION) ) { + } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y) ) { setZRotation(-value); - } else if( axis == axis.getJoystick().getAxis(JoystickAxis.LEFT_TRIGGER) ) { + } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_TRIGGER) ) { if( axis.getJoystick().getButton(JoystickButton.BUTTON_6) == null ) { // left/right triggers sometimes only show up as axes boolean pressed = value != 0; @@ -318,7 +325,7 @@ public void setAxisValue( JoystickAxis axis, float value ) { setButtonValue(JoystickButton.BUTTON_6, pressed); } } - } else if( axis == axis.getJoystick().getAxis(JoystickAxis.RIGHT_TRIGGER) ) { + } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER) ) { if( axis.getJoystick().getButton(JoystickButton.BUTTON_7) == null ) { // left/right triggers sometimes only show up as axes boolean pressed = value != 0; diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle index 5e929e8c2f..e360ab7054 100644 --- a/jme3-lwjgl3/build.gradle +++ b/jme3-lwjgl3/build.gradle @@ -12,6 +12,7 @@ dependencies { api libs.lwjgl3.openal api libs.lwjgl3.opencl api libs.lwjgl3.opengl + api libs.lwjgl3.sdl runtimeOnly(variantOf(libs.lwjgl3.base){ classifier('natives-windows') }) runtimeOnly(variantOf(libs.lwjgl3.base){ classifier('natives-windows-x86') }) @@ -52,6 +53,14 @@ dependencies { runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-linux-arm64') }) runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-macos') }) runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-macos-arm64') }) + + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-windows') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-windows-x86') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux-arm32') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux-arm64') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-macos') }) + runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-macos-arm64') }) } javadoc { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java index 39f0fa6b5a..68dfa45c94 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java @@ -34,30 +34,65 @@ import com.jme3.input.*; import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyButtonEvent; +import com.jme3.math.FastMath; + import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; import java.util.logging.Logger; +import com.jme3.system.AppSettings; +import org.lwjgl.glfw.GLFWGamepadState; import static org.lwjgl.glfw.GLFW.*; /** * The LWJGL implementation of {@link JoyInput}. * - * @author Daniel Johansson (dannyjo) + * @author Daniel Johansson (dannyjo), Riccardo Balbo * @since 3.1 */ public class GlfwJoystickInput implements JoyInput { - private static final Logger LOGGER = Logger.getLogger(GlfwJoystickInput.class.getName()); + private static final int POV_X_AXIS_ID = 7; + private static final int POV_Y_AXIS_ID = 8; - private RawInputListener listener; - + private final AppSettings settings; private final Map joysticks = new HashMap<>(); - private final Map joyButtonPressed = new HashMap<>(); + private final Map joyAxisValues = new HashMap<>(); private boolean initialized = false; + private float virtualTriggerThreshold; + private boolean xboxStyle; + private float globalJitterThreshold = 0f; + private GLFWGamepadState gamepadState; + private RawInputListener listener; + + public GlfwJoystickInput(AppSettings settings) { + this.settings = settings; + try { + String path = settings.getSDLGameControllerDBResourcePath(); + if (path != null && !path.trim().isEmpty()) { + ByteBuffer bbf = SdlGameControllerDb.getGamecontrollerDb(path); + if (!glfwUpdateGamepadMappings(bbf)) throw new Exception("Failed to load"); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to load gamecontrollerdb, fallback to glfw default mappings", + e); + } + } + + @Override + public void initialize() { + gamepadState = GLFWGamepadState.create(); + + virtualTriggerThreshold = settings.getJoysticksTriggerToButtonThreshold(); + xboxStyle = settings.getJoysticksMapper().equals(AppSettings.JOYSTICKS_XBOX_LEGACY_MAPPER); + globalJitterThreshold = settings.getJoysticksAxisJitterThreshold(); + + initialized = true; + } @Override public void setJoyRumble(final int joyId, final float amount) { @@ -78,9 +113,9 @@ public void fireJoystickDisconnectedEvent(int jid) { public void reloadJoysticks() { joysticks.clear(); - + joyButtonPressed.clear(); + joyAxisValues.clear(); InputManager inputManager = (InputManager) listener; - Joystick[] joysticks = loadJoysticks(inputManager); inputManager.setJoysticks(joysticks); } @@ -90,43 +125,95 @@ public Joystick[] loadJoysticks(final InputManager inputManager) { for (int i = 0; i < GLFW_JOYSTICK_LAST; i++) { if (glfwJoystickPresent(i)) { - final String name = glfwGetJoystickName(i); - final GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name); - joysticks.put(i, joystick); - - final FloatBuffer floatBuffer = glfwGetJoystickAxes(i); - - int axisIndex = 0; - while (floatBuffer.hasRemaining()) { - floatBuffer.get(); - - final String logicalId = JoystickCompatibilityMappings.remapAxis(joystick.getName(), convertAxisIndex(axisIndex)); - final JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, convertAxisIndex(axisIndex), logicalId, true, false, 0.0f); - joystick.addAxis(axisIndex, joystickAxis); - axisIndex++; + boolean isGlfwGamepad = xboxStyle && glfwJoystickIsGamepad(i); + + String name; + if (isGlfwGamepad) { + name = glfwGetGamepadName(i); + } else { + name = glfwGetJoystickName(i); + LOGGER.log(Level.WARNING, + "Unknown controller detected: {0} - guid: {1}. Fallback to raw input handling", + new Object[] { name, glfwGetJoystickGUID(i) }); } - final ByteBuffer byteBuffer = glfwGetJoystickButtons(i); + GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name, isGlfwGamepad); + joysticks.put(i, joystick); - if (byteBuffer != null) { - int buttonIndex = 0; - while (byteBuffer.hasRemaining()) { - byteBuffer.get(); + if(!isGlfwGamepad){ + // RAW axis + FloatBuffer floatBuffer = glfwGetJoystickAxes(i); + if (floatBuffer == null) continue; + + int axisIndex = 0; + while (floatBuffer.hasRemaining()) { + floatBuffer.get(); + + String logicalId = JoystickCompatibilityMappings.remapAxis(joystick.getName(), + convertAxisIndex(axisIndex)); + JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, + convertAxisIndex(axisIndex), logicalId, true, false, 0.0f); + joystick.addAxis(axisIndex, joystickAxis); + axisIndex++; + } - final String logicalId = JoystickCompatibilityMappings.remapButton(joystick.getName(), String.valueOf(buttonIndex)); - final JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex, String.valueOf(buttonIndex), logicalId); + // raw buttons + ByteBuffer byteBuffer = glfwGetJoystickButtons(i); + + if (byteBuffer != null) { + int buttonIndex = 0; + while (byteBuffer.hasRemaining()) { + byteBuffer.get(); + + String logicalId = JoystickCompatibilityMappings.remapButton(joystick.getName(), + String.valueOf(buttonIndex)); + JoystickButton button = new DefaultJoystickButton(inputManager, joystick, + buttonIndex, String.valueOf(buttonIndex), logicalId); + joystick.addButton(button); + joyButtonPressed.put(button, false); + buttonIndex++; + } + } + } else { + // Managed axis + for (int axisIndex = 0; axisIndex <= GLFW_GAMEPAD_AXIS_LAST; axisIndex++) { + String logicalId = remapAxisToJme(axisIndex); + if (logicalId == null) continue; + String axisName = logicalId; // no need to remap with JoystickCompatibilityMappings as + // glfw already handles remapping + JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, + axisName, logicalId, true, false, 0.0f); + joystick.addAxis(axisIndex, axis); + } + + // Virtual POV axes for D-pad. + JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, POV_X_AXIS_ID, + JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0.0f); + joystick.addAxis(POV_X_AXIS_ID, povX); + + JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, POV_Y_AXIS_ID, + JoystickAxis.POV_Y, JoystickAxis.POV_Y, true, false, 0.0f); + joystick.addAxis(POV_Y_AXIS_ID, povY); + + // managed buttons + for (int buttonIndex = 0; buttonIndex <= GLFW_GAMEPAD_BUTTON_LAST; buttonIndex++) { + String logicalId = remapButtonToJme(buttonIndex); + if (logicalId == null) continue; + String buttonName = logicalId; + JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex, + buttonName, logicalId); joystick.addButton(button); joyButtonPressed.put(button, false); - buttonIndex++; } } + } } return joysticks.values().toArray(new GlfwJoystick[joysticks.size()]); } - - private String convertAxisIndex(final int index) { + + private String convertAxisIndex(int index) { if (index == 0) { return "pov_x"; } else if (index == 1) { @@ -136,52 +223,194 @@ private String convertAxisIndex(final int index) { } else if (index == 3) { return "rz"; } - return String.valueOf(index); } - @Override - public void initialize() { - initialized = true; - } - @Override public void update() { float rawValue, value; - for (final Map.Entry entry : joysticks.entrySet()) { + for (Map.Entry entry : joysticks.entrySet()) { + if (!glfwJoystickPresent(entry.getKey())) continue; + if (!entry.getValue().isGlfwGamepad()) { + + // Axes + FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey()); + + // if a joystick is added or removed, the callback reloads the joysticks. + // when the callback is called and reloads the joystick, this iterator may already have started iterating. + // To avoid a NullPointerException we null-check the axisValues and bytebuffer objects. + // If the joystick it's iterating over no-longer exists it will return null. + + if (axisValues != null) { + for (JoystickAxis axis : entry.getValue().getAxes()) { + rawValue = axisValues.get(axis.getAxisId()); + value = JoystickCompatibilityMappings.remapAxisRange(axis, rawValue); + // listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue)); + updateAxis(axis, value, rawValue); + } + } + + // Buttons + ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey()); - // Axes - final FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey()); + if (byteBuffer != null) { + for (JoystickButton button : entry.getValue().getButtons()) { + boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS; + updateButton(button, pressed); + } + } + } else { + if (!glfwGetGamepadState(entry.getKey(), gamepadState)) return; + Joystick joystick = entry.getValue(); - // if a joystick is added or removed, the callback reloads the joysticks. - // when the callback is called and reloads the joystick, this iterator may already have started iterating. - // To avoid a NullPointerException we null-check the axisValues and bytebuffer objects. - // If the joystick it's iterating over no-longer exists it will return null. + FloatBuffer axes = gamepadState.axes(); - if (axisValues != null) { - for (final JoystickAxis axis : entry.getValue().getAxes()) { - rawValue = axisValues.get(axis.getAxisId()); - value = JoystickCompatibilityMappings.remapAxisRange(axis, rawValue); - listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue)); + // handle axes (skip virtual POV axes) + for (JoystickAxis axis : entry.getValue().getAxes()) { + int axisId = axis.getAxisId(); + if (axisId == POV_X_AXIS_ID || axisId == POV_Y_AXIS_ID) continue; + if (axisId < 0 || axisId > GLFW_GAMEPAD_AXIS_LAST) continue; + + rawValue = axes.get(axisId); + rawValue = remapAxisValueToJme(axisId, rawValue); + value = rawValue; // scaling handled by GLFW + + updateAxis(axis, value, rawValue); } - } - // Buttons - final ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey()); + // virtual trigger buttons + if (virtualTriggerThreshold > 0.0f) { + float leftTrigger = remapAxisValueToJme(GLFW_GAMEPAD_AXIS_LEFT_TRIGGER, + axes.get(GLFW_GAMEPAD_AXIS_LEFT_TRIGGER)); + float rightTrigger = remapAxisValueToJme(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, + axes.get(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER)); + updateButton(joystick.getButton(JoystickButton.BUTTON_XBOX_LT), + leftTrigger > virtualTriggerThreshold); + updateButton(joystick.getButton(JoystickButton.BUTTON_XBOX_RT), + rightTrigger > virtualTriggerThreshold); + } - if (byteBuffer != null) { - for (final JoystickButton button : entry.getValue().getButtons()) { - final boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS; + ByteBuffer buttons = gamepadState.buttons(); + + for (int btnIndex = 0; btnIndex <= GLFW_GAMEPAD_BUTTON_LAST; btnIndex++) { + String jmeButtonIndex = remapButtonToJme(btnIndex); + if (jmeButtonIndex == null) continue; - if (joyButtonPressed.get(button) != pressed) { - joyButtonPressed.put(button, pressed); - listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); - } + JoystickButton button = joystick.getButton(jmeButtonIndex); + if (button == null) continue; + + boolean pressed = buttons.get(btnIndex) == GLFW_PRESS; + updateButton(button, pressed); + } + + // D-pad to virtual POV axes + boolean dpadUp = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW_PRESS; + boolean dpadDown = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW_PRESS; + boolean dpadLeft = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW_PRESS; + boolean dpadRight = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW_PRESS; + + float povX = dpadLeft ? -1f : (dpadRight ? 1f : 0f); + float povY = dpadDown ? -1f : (dpadUp ? 1f : 0f); + + JoystickAxis povXAxis = joystick.getPovXAxis(); + if (povXAxis != null) { + updateAxis(povXAxis, povX, povX); + } + + JoystickAxis povYAxis = joystick.getPovYAxis(); + if (povYAxis != null) { + updateAxis(povYAxis, povY, povY); } } } } + + + private String remapAxisToJme(int glfwAxisIndex) { + switch (glfwAxisIndex) { + case GLFW_GAMEPAD_AXIS_LEFT_X: + return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X; + case GLFW_GAMEPAD_AXIS_LEFT_Y: + return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y; + case GLFW_GAMEPAD_AXIS_RIGHT_X: + return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X; + case GLFW_GAMEPAD_AXIS_RIGHT_Y: + return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y; + case GLFW_GAMEPAD_AXIS_LEFT_TRIGGER: + return JoystickAxis.AXIS_XBOX_LEFT_TRIGGER; + case GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER: + return JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER; + default: + return null; + + } + } + + private String remapButtonToJme(int glfwButtonIndex) { + switch (glfwButtonIndex) { + case GLFW_GAMEPAD_BUTTON_Y: + return JoystickButton.BUTTON_XBOX_Y; + case GLFW_GAMEPAD_BUTTON_B: + return JoystickButton.BUTTON_XBOX_B; + case GLFW_GAMEPAD_BUTTON_A: + return JoystickButton.BUTTON_XBOX_A; + case GLFW_GAMEPAD_BUTTON_X: + return JoystickButton.BUTTON_XBOX_X; + case GLFW_GAMEPAD_BUTTON_LEFT_BUMPER: + return JoystickButton.BUTTON_XBOX_LB; + case GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER: + return JoystickButton.BUTTON_XBOX_RB; + case GLFW_GAMEPAD_BUTTON_BACK: + return JoystickButton.BUTTON_XBOX_BACK; + case GLFW_GAMEPAD_BUTTON_START: + return JoystickButton.BUTTON_XBOX_START; + case GLFW_GAMEPAD_BUTTON_LEFT_THUMB: + return JoystickButton.BUTTON_XBOX_L3; + case GLFW_GAMEPAD_BUTTON_RIGHT_THUMB: + return JoystickButton.BUTTON_XBOX_R3; + case GLFW_GAMEPAD_BUTTON_DPAD_UP: + return JoystickButton.BUTTON_XBOX_DPAD_UP; + case GLFW_GAMEPAD_BUTTON_DPAD_DOWN: + return JoystickButton.BUTTON_XBOX_DPAD_DOWN; + case GLFW_GAMEPAD_BUTTON_DPAD_LEFT: + return JoystickButton.BUTTON_XBOX_DPAD_LEFT; + case GLFW_GAMEPAD_BUTTON_DPAD_RIGHT: + return JoystickButton.BUTTON_XBOX_DPAD_RIGHT; + default: + return null; + + } + } + + private static float remapAxisValueToJme(int axisId, float v) { + if (axisId == GLFW_GAMEPAD_AXIS_LEFT_TRIGGER || axisId == GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) { + if (v < -1f) v = -1f; + if (v > 1f) v = 1f; + return (v + 1f) * 0.5f; + } + return v; + } + + private void updateButton(JoystickButton button, boolean pressed) { + if (button == null) return; + Boolean old = joyButtonPressed.get(button); + if (old == null || old != pressed) { + joyButtonPressed.put(button, pressed); + listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); + } + } + + private void updateAxis(JoystickAxis axis, float value, float rawValue) { + if (axis == null) return; + Float old = joyAxisValues.get(axis); + float jitter = FastMath.clamp(Math.max(axis.getJitterThreshold(), globalJitterThreshold), 0f, 1f); + if (old == null || FastMath.abs(old - value) > jitter) { + joyAxisValues.put(axis, value); + listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue)); + } + } + @Override public void destroy() { initialized = false; @@ -199,41 +428,71 @@ public void setInputListener(final RawInputListener listener) { @Override public long getInputTimeNanos() { - return 0; + return (long) (glfwGetTime() * 1000000000); } - protected class GlfwJoystick extends AbstractJoystick { - + private static class GlfwJoystick extends AbstractJoystick { + private final boolean isGlfwGamepad; + private JoystickAxis xAxis; + private JoystickAxis yAxis; private JoystickAxis povAxisX; private JoystickAxis povAxisY; - public GlfwJoystick(final InputManager inputManager, final JoyInput joyInput, final int joyId, final String name) { + public GlfwJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name, + boolean gamepad) { super(inputManager, joyInput, joyId, name); + this.isGlfwGamepad = gamepad; } - public void addAxis(final int index, final JoystickAxis axis) { - super.addAxis(axis); + public boolean isGlfwGamepad() { + return isGlfwGamepad; + } - if (index == 0) { - povAxisX = axis; - } else if (index == 1) { - povAxisY = axis; + public void addAxis(int index, JoystickAxis axis) { + super.addAxis(axis); + if (isGlfwGamepad) { + switch (index) { + case GLFW_GAMEPAD_AXIS_LEFT_X: { + xAxis = axis; + break; + } + case GLFW_GAMEPAD_AXIS_LEFT_Y: { + yAxis = axis; + break; + } + case POV_X_AXIS_ID: { + povAxisX = axis; + break; + } + case POV_Y_AXIS_ID: { + povAxisY = axis; + break; + } + } + } else { + if (index == 0) { + xAxis = axis; + povAxisX = axis; + } else if (index == 1) { + yAxis = axis; + povAxisY = axis; + } } } @Override - protected void addButton(final JoystickButton button) { + protected void addButton(JoystickButton button) { super.addButton(button); } @Override public JoystickAxis getXAxis() { - return povAxisX; + return xAxis; } @Override public JoystickAxis getYAxis() { - return povAxisY; + return yAxis; } @Override @@ -248,12 +507,13 @@ public JoystickAxis getPovYAxis() { @Override public int getXAxisIndex() { - return povAxisX.getAxisId(); + return xAxis != null ? xAxis.getAxisId() : 0; } @Override public int getYAxisIndex() { - return povAxisY.getAxisId(); + return yAxis != null ? yAxis.getAxisId() : 1; } } + } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlGameControllerDb.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlGameControllerDb.java new file mode 100644 index 0000000000..5f7c6020ea --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlGameControllerDb.java @@ -0,0 +1,39 @@ +package com.jme3.input.lwjgl; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.jme3.util.BufferUtils; +import com.jme3.util.res.Resources; + +public class SdlGameControllerDb { + private static final Logger LOGGER = Logger.getLogger(SdlGameControllerDb.class.getName()); + + public static ByteBuffer getGamecontrollerDb(String path) throws Exception { + try ( InputStream gamecontrollerdbIs = Resources.getResourceAsStream(path)) { + if(gamecontrollerdbIs == null) throw new Exception("Resource not found"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + byte data[] = new byte[4096]; + int read; + while ((read = gamecontrollerdbIs.read(data)) != -1) { + bos.write(data, 0, read); + } + data = bos.toByteArray(); + + ByteBuffer gamecontrollerdb = BufferUtils.createByteBuffer(data.length + 1); + gamecontrollerdb.put(data); + gamecontrollerdb.put((byte)0); // null-terminate + gamecontrollerdb.flip(); + LOGGER.log(Level.INFO, "Loaded gamecontrollerdb from {0}", path); + return gamecontrollerdb; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to load "+path+" ", e); + throw e; + } + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java new file mode 100644 index 0000000000..7e398d4096 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java @@ -0,0 +1,621 @@ +package com.jme3.input.lwjgl; + +import com.jme3.input.*; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import com.jme3.math.FastMath; +import com.jme3.system.AppSettings; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.lwjgl.sdl.*; +import org.lwjgl.system.MemoryStack; + +import static org.lwjgl.sdl.SDLInit.*; +import static org.lwjgl.sdl.SDLEvents.*; +import static org.lwjgl.sdl.SDLGamepad.*; +import static org.lwjgl.sdl.SDLJoystick.*; +import static org.lwjgl.sdl.SDLError.*; +import static org.lwjgl.sdl.SDLTimer.*; + +/** + * The SDL based implementation of {@link JoyInput}. + * + * @author Riccardo Balbo + */ +public class SdlJoystickInput implements JoyInput { + + private static final Logger LOGGER = Logger.getLogger(SdlJoystickInput.class.getName()); + private static final int POV_X_AXIS_ID = 7; + private static final int POV_Y_AXIS_ID = 8; + + private final AppSettings settings; + private final Map joysticks = new HashMap<>(); + private final Map joyButtonPressed = new HashMap<>(); + private final Map joyAxisValues = new HashMap<>(); + private final int flags = SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_EVENTS; + + private boolean initialized; + private float virtualTriggerThreshold; + private float globalJitterThreshold; + private boolean loadGamepads; + private boolean loadRaw; + + private RawInputListener listener; + + public SdlJoystickInput(AppSettings settings) { + this.settings = settings; + try { + String path = settings.getSDLGameControllerDBResourcePath(); + if (path != null && !path.trim().isEmpty()) { + ByteBuffer bbf = SdlGameControllerDb.getGamecontrollerDb(path); + if (SDL_AddGamepadMapping(bbf) == -1) { + throw new Exception("Failed to load"); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to load gamecontrollerdb, fallback to sdl default mappings", e); + } + } + + @Override + public void initialize() { + if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_EVENTS)) { + String err = SDL_GetError(); + throw new IllegalStateException("SDL_InitSubSystem failed: " + err); + } + virtualTriggerThreshold = settings.getJoysticksTriggerToButtonThreshold(); + globalJitterThreshold = settings.getJoysticksAxisJitterThreshold(); + + String mapper = settings.getJoysticksMapper(); + switch (mapper) { + case AppSettings.JOYSTICKS_RAW_MAPPER: + loadGamepads = false; + loadRaw = true; + break; + case AppSettings.JOYSTICKS_XBOX_MAPPER: + loadGamepads = true; + loadRaw = false; + break; + default: + case AppSettings.JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER: + loadGamepads = true; + loadRaw = true; + break; + } + + initialized = true; + } + + private void onDeviceConnected(int deviceIndex, boolean isGamepad) { + if (loadGamepads && !isGamepad && SDL_IsGamepad(deviceIndex)) { + // SDL will fire both GAMEPAD and JOYSTICK events for recognized + // gamepads, so here we check if the joystick is expected to be + // a gamepad, and skip it if so to avoid duplicates. + return; + } + + InputManager inputManager = (InputManager) listener; + + SdlJoystick joystick; + if (isGamepad) { + long gamepad = SDL_OpenGamepad(deviceIndex); + if (gamepad == 0L) { + LOGGER.log(Level.FINE, "SDL failed to open gamepad for id {0}: {1}", + new Object[] { deviceIndex, SDL_GetError() }); + return; + } + + String name = SDL_GetGamepadName(gamepad); + + joystick = new SdlJoystick(inputManager, this, deviceIndex, name, gamepad, 0L); + joysticks.put(deviceIndex, joystick); + + // Managed axes (standard layout) + for (int axisIndex = 0; axisIndex < SDL_GAMEPAD_AXIS_COUNT; axisIndex++) { + String logicalId = remapAxisToJme(axisIndex); + if (logicalId == null) continue; + + String axisName = getAxisLabel(joystick, axisIndex); + + JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, axisName, + logicalId, true, false, 0.0f); + joystick.addAxis(axisIndex, axis); + } + + // Managed buttons: map SDL gamepad buttons into your JME logical ids + for (int buttonIndex = 0; buttonIndex < SDL_GAMEPAD_BUTTON_COUNT; buttonIndex++) { + String logicalId = remapButtonToJme(buttonIndex); + if (logicalId == null) continue; + String buttonName = getButtonLabel(joystick, buttonIndex); + + JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex, + buttonName, logicalId); + joystick.addButton(button); + joyButtonPressed.put(button, false); + } + + } else { + long joy = SDL_OpenJoystick(deviceIndex); + if (joy == 0L) return; + + String name = SDL_GetJoystickName(joy); + joystick = new SdlJoystick(inputManager, this, deviceIndex, name, 0L, joy); + joysticks.put(deviceIndex, joystick); + + int numAxes = SDL_GetNumJoystickAxes(joy); + int numButtons = SDL_GetNumJoystickButtons(joy); + + for (int axisIndex = 0; axisIndex < numAxes; axisIndex++) { + String logicalId = String.valueOf(axisIndex); + String axisName = logicalId; + + JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, axisName, + logicalId, true, false, 0.0f); + + joystick.addAxis(axisIndex, axis); + } + + for (int buttonIndex = 0; buttonIndex < numButtons; buttonIndex++) { + String logicalId = String.valueOf(buttonIndex); + String buttonName = logicalId; + JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex, + buttonName, logicalId); + joystick.addButton(button); + } + } + + // Virtual POV axes for D-pad. + JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, POV_X_AXIS_ID, JoystickAxis.POV_X, + JoystickAxis.POV_X, true, false, 0.0f); + joystick.addAxis(POV_X_AXIS_ID, povX); + + JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, POV_Y_AXIS_ID, JoystickAxis.POV_Y, + JoystickAxis.POV_Y, true, false, 0.0f); + joystick.addAxis(POV_Y_AXIS_ID, povY); + + ((InputManager) listener).fireJoystickConnectedEvent(joystick); + + } + + private void destroyJoystick(SdlJoystick joystick) { + if (joystick.isGamepad()) { + if (joystick.gamepad != 0L) { + SDL_CloseGamepad(joystick.gamepad); + } + } else { + if (joystick.joystick != 0L) { + SDL_CloseJoystick(joystick.joystick); + } + } + } + + private void onDeviceDisconnected(int deviceIndex) { + SdlJoystick joystick = joysticks.get(deviceIndex); + if (joystick == null) return; + + // clear all states associated with this joystick + joyButtonPressed.entrySet().removeIf(e -> e.getKey().getJoystick() == joystick); + joyAxisValues.entrySet().removeIf(e -> e.getKey().getJoystick() == joystick); + joysticks.remove(deviceIndex); + + // free resources + destroyJoystick(joystick); + + ((InputManager) listener).fireJoystickDisconnectedEvent(joystick); + } + + @Override + public Joystick[] loadJoysticks(InputManager inputManager) { + + for (SdlJoystick js : joysticks.values()) destroyJoystick(js); + joysticks.clear(); + + joyButtonPressed.clear(); + joyAxisValues.clear(); + + if (loadGamepads) { + // load managed gamepads + IntBuffer gamepads = SDL_GetGamepads(); + if (gamepads != null) { + while (gamepads.hasRemaining()) { + int deviceId = gamepads.get(); + onDeviceConnected(deviceId, true); + } + } + } + + if (loadRaw) { + // load raw gamepads + IntBuffer joys = SDL_GetJoysticks(); + if (joys != null) { + while (joys.hasRemaining()) { + int deviceId = joys.get(); + onDeviceConnected(deviceId, false); + } + } + } + + return joysticks.values().toArray(new Joystick[0]); + } + + @Override + public void update() { + handleConnectionEvents(); + handleInputEvents(); + } + + private void handleInputEvents() { + float rawValue, value; + for (SdlJoystick js : joysticks.values()) { + if (js.isGamepad()) { + long gp = js.gamepad; + + // for(int axisIndex=0; axisIndex 0f) { + if (jmeAxisId == JoystickAxis.AXIS_XBOX_LEFT_TRIGGER) { + updateButton(js.getButton(JoystickButton.BUTTON_XBOX_LT), + value > virtualTriggerThreshold); + } else if (jmeAxisId == JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER) { + updateButton(js.getButton(JoystickButton.BUTTON_XBOX_RT), + value > virtualTriggerThreshold); + } + } + + // Dpad -> virtual POV axes + float povXValue = 0f; + float povYValue = 0f; + + // button handling + // for (int b = 0; b <= SDL_GAMEPAD_BUTTON_COUNT; b++) { + for (JoystickButton button : js.getButtons()) { + int b = button.getButtonId(); + String jmeButtonId = button.getLogicalId(); + + boolean pressed = SDL_GetGamepadButton(gp, b); + updateButton(button, pressed); + + // Dpad -> virtual POV axes + if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_UP) { + povYValue += pressed ? 1f : 0f; + } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_DOWN) { + povYValue += pressed ? -1f : 0f; + } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_LEFT) { + povXValue += pressed ? -1f : 0f; + } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_RIGHT) { + povXValue += pressed ? 1f : 0f; + } + } + + JoystickAxis povXAxis = js.getPovXAxis(); + if (povXAxis != null) { + updateAxis(povXAxis, povXValue, povXValue); + } + + JoystickAxis povYAxis = js.getPovYAxis(); + if (povYAxis != null) { + updateAxis(povYAxis, povYValue, povYValue); + } + } + } else { + long joy = js.joystick; + + for (JoystickAxis axis : js.getAxes()) { + short v = SDL_GetJoystickAxis(joy, axis.getAxisId()); + rawValue = v; + value = v; + updateAxis(axis, value, rawValue); + } + + for (JoystickButton button : js.getButtons()) { + boolean pressed = SDL_GetJoystickButton(joy, button.getButtonId()); + updateButton(button, pressed); + } + } + } + } + + private void handleConnectionEvents() { + try (MemoryStack stack = MemoryStack.stackPush()) { + SDL_Event evt = SDL_Event.malloc(stack); + while (SDL_PollEvent(evt)) { + int type = evt.type(); + if (type == SDL_EVENT_GAMEPAD_ADDED) { + if (loadGamepads) { + int which = evt.gdevice().which(); + onDeviceConnected(which, true); + } + } else if (type == SDL_EVENT_GAMEPAD_REMOVED) { + int which = evt.gdevice().which(); + onDeviceDisconnected(which); + } else if (type == SDL_EVENT_JOYSTICK_ADDED) { + if (loadRaw) { + int which = evt.jdevice().which(); + onDeviceConnected(which, false); + } + } else if (type == SDL_EVENT_JOYSTICK_REMOVED) { + int which = evt.jdevice().which(); + onDeviceDisconnected(which); + } + } + } + } + + @Override + public void setJoyRumble(int joyId, float amount) { + setJoyRumble(joyId, amount, amount, 100f / 1000f); + } + + public void setJoyRumble(int joyId, float highFrequency, float lowFrequency, float duration) { + SdlJoystick js = joysticks.get(joyId); + if (js == null) return; + + highFrequency = FastMath.clamp(highFrequency, 0f, 1f); + lowFrequency = FastMath.clamp(lowFrequency, 0f, 1f); + + if (js.isGamepad() && js.gamepad != 0L) { + int ampHigh = (int) (highFrequency * 0xFFFF); + int ampLow = (int) (lowFrequency * 0xFFFF); + int durationMs = (int) (duration * 1000f); + SDL_RumbleGamepad(js.gamepad, (short) ampHigh, (short) ampLow, durationMs); + } + } + + private String getButtonLabel(SdlJoystick gamepad, int sdlButtonIndex) { + int label = SDL_GetGamepadButtonLabel(gamepad.gamepad, sdlButtonIndex); + switch (label) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "A"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "B"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "X"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "Y"; + + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "CROSS"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "CIRCLE"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "SQUARE"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "TRIANGLE"; + + case SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN: + default: + return "" + sdlButtonIndex; + } + } + + private String getAxisLabel(SdlJoystick gamepad, int sdlAxisIndex) { + switch (sdlAxisIndex) { + case SDL_GAMEPAD_AXIS_LEFTX: + return "LEFT THUMB STICK (X)"; + case SDL_GAMEPAD_AXIS_LEFTY: + return "LEFT THUMB STICK (Y)"; + case SDL_GAMEPAD_AXIS_RIGHTX: + return "RIGHT THUMB STICK (X)"; + case SDL_GAMEPAD_AXIS_RIGHTY: + return "RIGHT THUMB STICK (Y)"; + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + return "LEFT TRIGGER"; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + return "RIGHT TRIGGER"; + default: + return "" + sdlAxisIndex; + } + } + + private String remapAxisToJme(int sdlAxisIndex) { + switch (sdlAxisIndex) { + case SDL_GAMEPAD_AXIS_LEFTX: + return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X; + case SDL_GAMEPAD_AXIS_LEFTY: + return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y; + case SDL_GAMEPAD_AXIS_RIGHTX: + return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X; + case SDL_GAMEPAD_AXIS_RIGHTY: + return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y; + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + return JoystickAxis.AXIS_XBOX_LEFT_TRIGGER; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + return JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER; + default: + return null; + } + } + + private String remapButtonToJme(int sdlButtonIndex) { + switch (sdlButtonIndex) { + case SDL_GAMEPAD_BUTTON_NORTH: + return JoystickButton.BUTTON_XBOX_Y; + case SDL_GAMEPAD_BUTTON_EAST: + return JoystickButton.BUTTON_XBOX_B; + case SDL_GAMEPAD_BUTTON_SOUTH: + return JoystickButton.BUTTON_XBOX_A; + case SDL_GAMEPAD_BUTTON_WEST: + return JoystickButton.BUTTON_XBOX_X; + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: + return JoystickButton.BUTTON_XBOX_LB; + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return JoystickButton.BUTTON_XBOX_RB; + case SDL_GAMEPAD_BUTTON_BACK: + return JoystickButton.BUTTON_XBOX_BACK; + case SDL_GAMEPAD_BUTTON_START: + return JoystickButton.BUTTON_XBOX_START; + case SDL_GAMEPAD_BUTTON_LEFT_STICK: + return JoystickButton.BUTTON_XBOX_L3; + case SDL_GAMEPAD_BUTTON_RIGHT_STICK: + return JoystickButton.BUTTON_XBOX_R3; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + return JoystickButton.BUTTON_XBOX_DPAD_UP; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + return JoystickButton.BUTTON_XBOX_DPAD_DOWN; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + return JoystickButton.BUTTON_XBOX_DPAD_LEFT; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + return JoystickButton.BUTTON_XBOX_DPAD_RIGHT; + default: + return null; + } + } + + private float remapAxisValueToJme(int axisId, short v) { + if (axisId == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || axisId == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + // [0..32767] -> [0..1] + if (v <= 0) return 0f; + return Math.min(1f, v / 32767f); + } else { + // [-32768..32767] -> [-1..1] + if (v == Short.MIN_VALUE) return -1f; + return v / 32767f; + } + } + + private void updateButton(JoystickButton button, boolean pressed) { + if (button == null) return; + Boolean old = joyButtonPressed.get(button); + if (old == null || old != pressed) { + joyButtonPressed.put(button, pressed); + listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); + } + } + + private void updateAxis(JoystickAxis axis, float value, float rawValue) { + if (axis == null) return; + Float old = joyAxisValues.get(axis); + float jitter = FastMath.clamp(Math.max(axis.getJitterThreshold(), globalJitterThreshold), 0f, 1f); + if (old == null || FastMath.abs(old - value) > jitter) { + joyAxisValues.put(axis, value); + listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue)); + } + } + + @Override + public void destroy() { + // Close devices + for (SdlJoystick js : joysticks.values()) { + if (js.gamepad != 0L) SDL_CloseGamepad(js.gamepad); + if (js.joystick != 0L) SDL_CloseJoystick(js.joystick); + } + joysticks.clear(); + + // Quit subsystems + SDL_QuitSubSystem(flags); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void setInputListener(final RawInputListener listener) { + this.listener = listener; + } + + @Override + public long getInputTimeNanos() { + return SDL_GetTicksNS(); + } + + private static class SdlJoystick extends AbstractJoystick { + + private JoystickAxis xAxis; + private JoystickAxis yAxis; + private JoystickAxis povAxisX; + private JoystickAxis povAxisY; + + long gamepad; + long joystick; + + SdlJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name, long gamepad, + long joystick) { + super(inputManager, joyInput, joyId, name); + this.gamepad = gamepad; + this.joystick = joystick; + + } + + boolean isGamepad() { + return gamepad != 0L; + } + + void addAxis(int index, JoystickAxis axis) { + super.addAxis(axis); + switch (index) { + case SDL_GAMEPAD_AXIS_LEFTX: { + xAxis = axis; + break; + } + case SDL_GAMEPAD_AXIS_LEFTY: { + yAxis = axis; + break; + } + case POV_X_AXIS_ID: { + povAxisX = axis; + break; + } + case POV_Y_AXIS_ID: { + povAxisY = axis; + break; + } + } + + } + + @Override + public JoystickAxis getXAxis() { + return xAxis; + } + + @Override + public JoystickAxis getYAxis() { + return yAxis; + } + + @Override + public JoystickAxis getPovXAxis() { + return povAxisX; + } + + @Override + public JoystickAxis getPovYAxis() { + return povAxisY; + } + + @Override + public int getXAxisIndex() { + return xAxis != null ? xAxis.getAxisId() : 0; + } + + @Override + public int getYAxisIndex() { + return yAxis != null ? yAxis.getAxisId() : 1; + } + + @Override + public void addButton(JoystickButton button) { + super.addButton(button); + } + + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index 0a0a6bcd54..535fd295b1 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -32,6 +32,7 @@ package com.jme3.system.lwjgl; +import com.jme3.input.JoyInput; import com.jme3.input.lwjgl.GlfwJoystickInput; import com.jme3.input.lwjgl.GlfwKeyInput; import com.jme3.input.lwjgl.GlfwMouseInput; @@ -124,7 +125,7 @@ public abstract class LwjglContext implements JmeContext { protected GlfwKeyInput keyInput; protected GlfwMouseInput mouseInput; - protected GlfwJoystickInput joyInput; + protected JoyInput joyInput; protected Timer timer; @@ -275,17 +276,18 @@ private void initContext(boolean first) { GLFW.glfwSetJoystickCallback(new GLFWJoystickCallback() { @Override public void invoke(int jid, int event) { + if (!(joyInput instanceof GlfwJoystickInput)) return; // Invoke the disconnected event before we reload the joysticks or lose the reference to it. // Invoke the connected event after we reload the joysticks to obtain the reference to it. - - if ( event == GLFW.GLFW_CONNECTED ) { - joyInput.reloadJoysticks(); - joyInput.fireJoystickConnectedEvent(jid); - } - else { - joyInput.fireJoystickDisconnectedEvent(jid); - joyInput.reloadJoysticks(); + GlfwJoystickInput glfwJoyInput = (GlfwJoystickInput) joyInput; + + if (event == GLFW.GLFW_CONNECTED) { + glfwJoyInput.reloadJoysticks(); + glfwJoyInput.fireJoystickConnectedEvent(jid); + } else { + glfwJoyInput.fireJoystickDisconnectedEvent(jid); + glfwJoyInput.reloadJoysticks(); } } }); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index 0cc59b07fc..5854a76424 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -42,6 +42,7 @@ import com.jme3.input.lwjgl.GlfwJoystickInput; import com.jme3.input.lwjgl.GlfwKeyInput; import com.jme3.input.lwjgl.GlfwMouseInput; +import com.jme3.input.lwjgl.SdlJoystickInput; import com.jme3.math.Vector2f; import com.jme3.system.AppSettings; import com.jme3.system.Displays; @@ -53,11 +54,9 @@ import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; -import java.nio.IntBuffer; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -834,7 +833,19 @@ public void run() { @Override public JoyInput getJoyInput() { if (joyInput == null) { - joyInput = new GlfwJoystickInput(); + boolean useSdl = true; + + String mapper = settings.getJoysticksMapper(); + if (AppSettings.JOYSTICKS_LEGACY_MAPPER.equals(mapper) + || AppSettings.JOYSTICKS_XBOX_LEGACY_MAPPER.equals(mapper)) { + useSdl = false; + } + + if (useSdl) { + joyInput = new SdlJoystickInput(settings); + } else { + joyInput = new GlfwJoystickInput(settings); + } } return joyInput; }