From 20e662f9278d88177e1a8650b3185087c59dd1a9 Mon Sep 17 00:00:00 2001 From: isXander Date: Fri, 14 Apr 2023 11:55:30 +0100 Subject: [PATCH] 1.1 changelog, bump version, update comparison --- .../dev/isxander/controlify/Controlify.java | 4 - .../controller/AbstractController.java | 6 +- .../controlify/controller/Controller.java | 5 +- .../controller/gamepad/GamepadController.java | 72 ++---- .../controller/gamepad/GamepadState.java | 44 ---- .../controller/hid/ControllerHIDService.java | 27 +- .../joystick/SingleJoystickController.java | 14 +- .../driver/BasicGamepadInputDriver.java | 23 ++ .../isxander/controlify/driver/Driver.java | 8 + .../controlify/driver/GLFWGamepadDriver.java | 54 ++++ .../controlify/driver/GamepadDrivers.java | 33 +++ .../controlify/driver/GyroDriver.java | 25 ++ .../controlify/driver/RumbleDriver.java | 23 ++ .../controlify/driver/SDL2GamepadDriver.java | 59 +++++ .../controlify/driver/SteamDeckDriver.java | 234 ++++++++++++++++++ .../mixins/core/MinecraftMixin.java | 6 + 16 files changed, 511 insertions(+), 126 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/driver/BasicGamepadInputDriver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/Driver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/GLFWGamepadDriver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java create mode 100644 src/main/java/dev/isxander/controlify/driver/GyroDriver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/RumbleDriver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java create mode 100644 src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 41f85d4..22aeb46 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -333,11 +333,7 @@ public class Controlify implements ControlifyApi { if (this.currentController == controller) return; - if (this.currentController != null) - this.currentController.close(); - this.currentController = controller; - this.currentController.open(); if (switchableController == controller) { switchableController = null; diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index d42542e..78435ef 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -25,7 +25,7 @@ public abstract class AbstractController bindings; + protected ControllerBindings bindings; protected C config, defaultConfig; public AbstractController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { @@ -37,7 +37,7 @@ public abstract class AbstractController(this); } public String name() { diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index ffffdf9..ee3a085 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -14,6 +14,7 @@ import dev.isxander.controlify.rumble.RumbleSource; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; +import org.hid4java.HidDevice; import org.lwjgl.glfw.GLFW; import java.util.HashMap; @@ -41,7 +42,6 @@ public interface Controller controller) { + controller.close(); CONTROLLERS.remove(controller.uid(), controller); } diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java index d3cbe70..47ebe85 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -1,28 +1,28 @@ package dev.isxander.controlify.controller.gamepad; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.hid.ControllerHIDService; -import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; -import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.driver.*; import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleSource; -import org.libsdl.SDL; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWGamepadState; +import java.util.Set; + public class GamepadController extends AbstractController { private GamepadState state = GamepadState.EMPTY; private GamepadState prevState = GamepadState.EMPTY; - private long gamepadPtr; - private boolean rumbleSupported, triggerRumbleSupported; private final RumbleManager rumbleManager; - private boolean hasGyro; private GamepadState.GyroState absoluteGyro = GamepadState.GyroState.ORIGIN; + private final GamepadDrivers drivers; + private final Set uniqueDrivers; + public GamepadController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { super(joystickId, hidInfo); if (!GLFW.glfwJoystickIsGamepad(joystickId)) @@ -31,10 +31,15 @@ public class GamepadController extends AbstractController(this); } @Override @@ -51,23 +56,16 @@ public class GamepadController extends AbstractController hid.path().equals(h.path())); + unconsumedControllerHIDs.removeIf(h -> hid.getFirst().getPath().equals(h.getFirst().getPath())); - return new ControllerHIDInfo(type, Optional.of(hid.path())); + return new ControllerHIDInfo(type, Optional.of(hid.getFirst())); } public boolean isDisabled() { @@ -75,7 +76,7 @@ public class ControllerHIDService { // add an unconsumed identifier that can be removed if not disconnected HIDIdentifier identifier = new HIDIdentifier(attachedDevice.getVendorId(), attachedDevice.getProductId()); if (isController(attachedDevice)) - unconsumedControllerHIDs.add(new HIDIdentifierWithPath(attachedDevice.getPath(), identifier)); + unconsumedControllerHIDs.add(new Pair<>(attachedDevice, identifier)); } } @@ -90,7 +91,7 @@ public class ControllerHIDService { removeList.add(deviceId); // remove device from unconsumed list - unconsumedControllerHIDs.removeIf(device -> this.attachedDevices.get(deviceId).getPath().equals(device.path())); + unconsumedControllerHIDs.removeIf(device -> this.attachedDevices.get(deviceId).getPath().equals(device.getFirst().getPath())); } } @@ -105,16 +106,12 @@ public class ControllerHIDService { boolean isGenericDesktopControlOrGameControl = device.getUsagePage() == 0x1 || device.getUsagePage() == 0x5; boolean isSelfIdentifiedController = CONTROLLER_USAGE_IDS.contains(device.getUsage()); - return ControllerType.getTypeMap().containsKey(new HIDIdentifier(device.getVendorId(), device.getProductId())) - || (isGenericDesktopControlOrGameControl && isSelfIdentifiedController); + return isControllerType || (isGenericDesktopControlOrGameControl && isSelfIdentifiedController); } - public record ControllerHIDInfo(ControllerType type, Optional path) { + public record ControllerHIDInfo(ControllerType type, Optional hidDevice) { public Optional createControllerUID() { - return path.map(p -> UUID.nameUUIDFromBytes(p.getBytes())).map(UUID::toString); + return hidDevice.map(HidDevice::getPath).map(p -> UUID.nameUUIDFromBytes(p.getBytes())).map(UUID::toString); } } - - private record HIDIdentifierWithPath(String path, HIDIdentifier identifier) { - } } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java index 64fe071..c823ea0 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java @@ -5,6 +5,7 @@ import com.google.gson.JsonElement; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping; @@ -31,6 +32,12 @@ public class SingleJoystickController extends AbstractController(this); } @Override @@ -117,13 +124,6 @@ public class SingleJoystickController extends AbstractController getUniqueDrivers() { + Set drivers = Collections.newSetFromMap(new IdentityHashMap<>()); + drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver)); + return drivers; + } + + public static GamepadDrivers forController(int jid, Optional hid) { + BasicGamepadInputDriver basicGamepadInputDriver = new GLFWGamepadDriver(jid); + GyroDriver gyroDriver = GyroDriver.UNSUPPORTED; + RumbleDriver rumbleDriver = RumbleDriver.UNSUPPORTED; + + if (SDL2NativesManager.isLoaded()) { + SDL2GamepadDriver sdl2Driver = new SDL2GamepadDriver(jid); + gyroDriver = sdl2Driver; + rumbleDriver = sdl2Driver; + } + + // broken + if (hid.isPresent() && SteamDeckDriver.isSteamDeck(hid.get()) && false) { + gyroDriver = new SteamDeckDriver(hid.get()); + } + + return new GamepadDrivers(basicGamepadInputDriver, gyroDriver, rumbleDriver); + } +} diff --git a/src/main/java/dev/isxander/controlify/driver/GyroDriver.java b/src/main/java/dev/isxander/controlify/driver/GyroDriver.java new file mode 100644 index 0000000..2e8cb05 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/driver/GyroDriver.java @@ -0,0 +1,25 @@ +package dev.isxander.controlify.driver; + +import dev.isxander.controlify.controller.gamepad.GamepadState; + +public interface GyroDriver extends Driver { + GamepadState.GyroState getGyroState(); + + boolean isGyroSupported(); + + GyroDriver UNSUPPORTED = new GyroDriver() { + @Override + public void update() { + } + + @Override + public GamepadState.GyroState getGyroState() { + return GamepadState.GyroState.ORIGIN; + } + + @Override + public boolean isGyroSupported() { + return false; + } + }; +} diff --git a/src/main/java/dev/isxander/controlify/driver/RumbleDriver.java b/src/main/java/dev/isxander/controlify/driver/RumbleDriver.java new file mode 100644 index 0000000..dd13d17 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/driver/RumbleDriver.java @@ -0,0 +1,23 @@ +package dev.isxander.controlify.driver; + +public interface RumbleDriver extends Driver { + boolean rumble(float strongMagnitude, float weakMagnitude); + + boolean isRumbleSupported(); + + RumbleDriver UNSUPPORTED = new RumbleDriver() { + @Override + public void update() { + } + + @Override + public boolean rumble(float strongMagnitude, float weakMagnitude) { + return false; + } + + @Override + public boolean isRumbleSupported() { + return false; + } + }; +} diff --git a/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java new file mode 100644 index 0000000..591cc38 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java @@ -0,0 +1,59 @@ +package dev.isxander.controlify.driver; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.gamepad.GamepadState; +import dev.isxander.controlify.debug.DebugProperties; +import org.libsdl.SDL; + +public class SDL2GamepadDriver implements GyroDriver, RumbleDriver { + private final long ptrGamepad; + private GamepadState.GyroState gyroDelta; + private final boolean isGyroSupported, isRumbleSupported; + + public SDL2GamepadDriver(int jid) { + this.ptrGamepad = SDL.SDL_GameControllerOpen(jid); + this.isGyroSupported = SDL.SDL_GameControllerHasSensor(ptrGamepad, SDL.SDL_SENSOR_GYRO); + this.isRumbleSupported = SDL.SDL_GameControllerHasRumble(ptrGamepad); + } + + @Override + public void update() { + if (isGyroSupported()) { + float[] gyro = new float[3]; + SDL.SDL_GameControllerGetSensorData(ptrGamepad, SDL.SDL_SENSOR_GYRO, gyro, 3); + gyroDelta = new GamepadState.GyroState(gyro[0], gyro[1], gyro[2]); + if (DebugProperties.PRINT_GYRO) Controlify.LOGGER.info("Gyro delta: " + gyroDelta); + } + SDL.SDL_GameControllerUpdate(); + } + + @Override + public boolean rumble(float strongMagnitude, float weakMagnitude) { + // duration of 0 is infinite + if (!SDL.SDL_GameControllerRumble(ptrGamepad, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) { + Controlify.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError()); + return false; + } + return true; + } + + @Override + public GamepadState.GyroState getGyroState() { + return gyroDelta; + } + + @Override + public boolean isGyroSupported() { + return isGyroSupported; + } + + @Override + public boolean isRumbleSupported() { + return isRumbleSupported; + } + + @Override + public void close() { + SDL.SDL_GameControllerClose(ptrGamepad); + } +} diff --git a/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java b/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java new file mode 100644 index 0000000..21bdc91 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java @@ -0,0 +1,234 @@ +package dev.isxander.controlify.driver; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.gamepad.GamepadState; +import dev.isxander.controlify.controller.hid.HIDIdentifier; +import org.hid4java.HidDevice; + +public class SteamDeckDriver implements GyroDriver, BasicGamepadInputDriver { + private static final int cInputRecordLen = 8; // Number of bytes that are read from the hid device per 1 byte of HID + private static final int cByteposInput = 4; // Position in the raw hid data where HID data byte is + private static final byte[] startMarker = new byte[] { 0x01, 0x00, 0x09, 0x40 }; // Beginning of every Steam deck HID frame + + private final HidDevice hidDevice; + + private GamepadState.GyroState gyroDelta = GamepadState.GyroState.ORIGIN; + private BasicGamepadState basicGamepadState = new BasicGamepadState(GamepadState.AxesState.EMPTY, GamepadState.ButtonState.EMPTY); + + public SteamDeckDriver(HidDevice hidDevice) { + this.hidDevice = hidDevice; + this.hidDevice.open(); + this.hidDevice.setNonBlocking(true); + } + + @Override + public void update() { + sendSomething(); + + byte[] data = new byte[64]; + int readCnt = hidDevice.read(data); + + if (readCnt == 0) { + Controlify.LOGGER.warn("No data available."); + } + if (readCnt == -1) { + Controlify.LOGGER.warn("Error reading data."); + } + + if (!checkData(data, readCnt)) return; + + Frame frame = Frame.fromBytes(data); + System.out.println(frame); + readFrame(frame); + } + + private void sendSomething() { + hidDevice.getFeatureReport(new byte[]{ (byte) 0x89 }, (byte) 0x0); + hidDevice.write(new byte[]{ (byte) 0x89 }, 2, (byte) 0x0); + } + + private void readFrame(Frame frame) { + gyroDelta = new GamepadState.GyroState( + frame.gyroAxisFrontToBack, + frame.gyroAxisTopToBottom, + frame.gyroAxisRightToLeft + ); + + basicGamepadState = new BasicGamepadState( + new GamepadState.AxesState( + frame.leftStickX, + frame.leftStickY, + frame.rightStickX, + frame.rightStickY, + frame.l2Analog, + frame.r2Analog + ), + new GamepadState.ButtonState( + ((frame.buttons1BitMap >> 7) & 1) == 1, + ((frame.buttons1BitMap >> 5) & 1) == 1, + ((frame.buttons1BitMap >> 6) & 1) == 1, + ((frame.buttons1BitMap >> 4) & 1) == 1, + ((frame.buttons1BitMap >> 3) & 1) == 1, + ((frame.buttons1BitMap >> 2) & 1) == 1, + ((frame.buttons1BitMap >> 12) & 1) == 1, + ((frame.buttons1BitMap >> 14) & 1) == 1, + ((frame.buttons1BitMap >> 13) & 1) == 1, + false, false, false, false, + ((frame.buttons1BitMap >> 1) & 1) == 1, + ((frame.buttons1BitMap >> 0) & 1) == 1 + ) + ); + } + + private boolean checkData(byte[] data, int readCnt) { + int first4Bytes = 0xFFFF0002; + int first4BytesAlt = 0xFFFF0001; + + boolean inputFail = readCnt < data.length; + boolean startMarkerFail = false; + + if (!inputFail) { + startMarkerFail = first4Bytes(data) != first4Bytes; + if (startMarkerFail && first4Bytes(data) == first4BytesAlt) { + startMarkerFail = false; + + for (int i = cByteposInput, j = 0; j < startMarker.length; j++, i += cInputRecordLen) { + if (data[i] != startMarker[j]) { + startMarkerFail = true; + break; + } + } + } + } + + return !inputFail && !startMarkerFail; + } + + private int first4Bytes(byte[] data) { + return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; + } + + @Override + public GamepadState.GyroState getGyroState() { + return gyroDelta; + } + + @Override + public BasicGamepadState getBasicGamepadState() { + return basicGamepadState; + } + + @Override + public boolean isGyroSupported() { + return true; + } + + @Override + public void close() { + hidDevice.close(); + } + + // https://github.com/kmicki/SteamDeckGyroDSU/blob/574745406011cc2433fc6f179446ecc836180aa4/inc/sdgyrodsu/sdhidframe.h + private record Frame( + int header, + int increment, + + // Buttons 1: + // .0 - R2 full pull + // .1 - L2 full pull + // .2 - R1 + // .3 - L1 + // .4 - Y + // .5 - B + // .6 - X + // .7 - A + // .12 - Select + // .13 - STEAM + // .14 - Start + // .15 - L5 + // .16 - R5 + // .17 - L trackpad click + // .18 - R trackpad click + // .19 - L trackpad touch + // .20 - R trackpad touch + // .22 - L3 + // .26 - R3 + int buttons1BitMap, + + // Buttons 2: + // .9 - L4 + // .10 - R4 + // .14 - L3 touch + // .15 - R3 touch + // .18 - (...) + int buttons2BitMap, + + short leftTrackpadX, + short leftTrackpadY, + short rightTrackpadX, + short rightTrackpadY, + + short accelAxisRightToLeft, + short accelAxisTopToBottom, + short accelAxisFrontToBack, + + short gyroAxisRightToLeft, + short gyroAxisTopToBottom, + short gyroAxisFrontToBack, + + short unknown1, + short unknown2, + short unknown3, + short unknown4, + + short l2Analog, + short r2Analog, + short leftStickX, + short leftStickY, + short rightStickX, + short rightStickY, + + short leftTrackpadPushForce, + short rightTrackpadPushForce, + short leftStickTouchCoverage, + short rightStickTouchCoverage + ) { + // i love github copilot + public static Frame fromBytes(byte[] bytes) { + return new Frame( + (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3], + (bytes[4] << 24) | (bytes[5] << 16) | (bytes[6] << 8) | bytes[7], + (bytes[8] << 24) | (bytes[9] << 16) | (bytes[10] << 8) | bytes[11], + (bytes[12] << 24) | (bytes[13] << 16) | (bytes[14] << 8) | bytes[15], + (short) ((bytes[16] << 8) | bytes[17]), + (short) ((bytes[18] << 8) | bytes[19]), + (short) ((bytes[20] << 8) | bytes[21]), + (short) ((bytes[22] << 8) | bytes[23]), + (short) ((bytes[24] << 8) | bytes[25]), + (short) ((bytes[26] << 8) | bytes[27]), + (short) ((bytes[28] << 8) | bytes[29]), + (short) ((bytes[30] << 8) | bytes[31]), + (short) ((bytes[32] << 8) | bytes[33]), + (short) ((bytes[34] << 8) | bytes[35]), + (short) ((bytes[36] << 8) | bytes[37]), + (short) ((bytes[38] << 8) | bytes[39]), + (short) ((bytes[40] << 8) | bytes[41]), + (short) ((bytes[42] << 8) | bytes[43]), + (short) ((bytes[44] << 8) | bytes[45]), + (short) ((bytes[46] << 8) | bytes[47]), + (short) ((bytes[48] << 8) | bytes[49]), + (short) ((bytes[50] << 8) | bytes[51]), + (short) ((bytes[52] << 8) | bytes[53]), + (short) ((bytes[54] << 8) | bytes[55]), + (short) ((bytes[56] << 8) | bytes[57]), + (short) ((bytes[58] << 8) | bytes[59]), + (short) ((bytes[60] << 8) | bytes[61]), + (short) ((bytes[62] << 8) | bytes[63]) + ); + } + } + + public static boolean isSteamDeck(HidDevice hid) { + return hid.getVendorId() == 0x28DE && hid.getProductId() == 0x1205; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java index 2cd6b58..7182fb0 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.mixins.core; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.gui.screen.BetaNoticeScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.toasts.SystemToast; @@ -57,4 +58,9 @@ public abstract class MinecraftMixin { }); return resourceReload; } + + @Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/telemetry/ClientTelemetryManager;close()V")) + private void onMinecraftClose(CallbackInfo ci) { + Controller.CONTROLLERS.values().forEach(Controller::close); + } }