1
0
forked from Clones/Controlify

1.1 changelog, bump version, update comparison

This commit is contained in:
isXander
2023-04-14 11:55:30 +01:00
parent 95e1ce2385
commit 20e662f927
16 changed files with 511 additions and 126 deletions

View File

@ -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;

View File

@ -25,7 +25,7 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
private final String guid;
private final ControllerType type;
private final ControllerBindings<S> bindings;
protected ControllerBindings<S> bindings;
protected C config, defaultConfig;
public AbstractController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
@ -37,7 +37,7 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
this.joystickId = joystickId;
this.guid = GLFW.glfwGetJoystickGUID(joystickId);
if (hidInfo.path().isPresent()) {
if (hidInfo.hidDevice().isPresent()) {
this.uid = hidInfo.createControllerUID().orElseThrow();
this.type = hidInfo.type();
} else {
@ -48,8 +48,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
var joystickName = GLFW.glfwGetJoystickName(joystickId);
String name = type != ControllerType.UNKNOWN || joystickName == null ? type.friendlyName() : joystickName;
setName(name);
this.bindings = new ControllerBindings<>(this);
}
public String name() {

View File

@ -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<S extends ControllerState, C extends ControllerConfi
void updateState();
void clearState();
default void open() {}
default void close() {}
RumbleManager rumbleManager();
@ -77,12 +77,13 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
CrashReportCategory category = crashReport.addCategory("Controller Info");
category.setDetail("Joystick ID", joystickId);
category.setDetail("Controller identification", hidInfo.type());
category.setDetail("HID path", hidInfo.path().orElse("N/A"));
category.setDetail("HID path", hidInfo.hidDevice().map(HidDevice::getPath).orElse("N/A"));
throw new ReportedException(crashReport);
}
}
static void remove(Controller<?, ?> controller) {
controller.close();
CONTROLLERS.remove(controller.uid(), controller);
}

View File

@ -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<GamepadState, GamepadConfig> {
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<Driver> uniqueDrivers;
public GamepadController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
if (!GLFW.glfwJoystickIsGamepad(joystickId))
@ -31,10 +31,15 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
if (!this.name.startsWith(type().friendlyName()))
setName(GLFW.glfwGetGamepadName(joystickId));
this.drivers = GamepadDrivers.forController(joystickId, hidInfo.hidDevice());
this.uniqueDrivers = drivers.getUniqueDrivers();
this.rumbleManager = new RumbleManager(this);
this.defaultConfig = new GamepadConfig();
this.config = new GamepadConfig();
this.bindings = new ControllerBindings<>(this);
}
@Override
@ -51,23 +56,16 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
public void updateState() {
prevState = state;
GamepadState.AxesState rawAxesState = GamepadState.AxesState.fromController(this);
GamepadState.AxesState axesState = rawAxesState
uniqueDrivers.forEach(Driver::update);
BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState();
GamepadState.AxesState deadzoneAxesState = basicState.axes()
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY)
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY);
GamepadState.ButtonState buttonState = GamepadState.ButtonState.fromController(this);
GamepadState.GyroState gyroDelta = null;
if (this.hasGyro) {
float[] gyro = new float[3];
SDL.SDL_GameControllerGetSensorData(gamepadPtr, 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);
absoluteGyro = absoluteGyro.add(gyroDelta);
}
SDL.SDL_GameControllerUpdate();
GamepadState.GyroState gyroState = drivers.gyroDriver().getGyroState();
state = new GamepadState(axesState, rawAxesState, buttonState, gyroDelta, absoluteGyro);
state = new GamepadState(deadzoneAxesState, basicState.axes(), basicState.buttons(), gyroState, absoluteGyro);
}
public GamepadState.GyroState absoluteGyroState() {
@ -75,7 +73,7 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
}
public boolean hasGyro() {
return hasGyro;
return drivers.gyroDriver().isGyroSupported();
}
@Override
@ -104,18 +102,12 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
strongMagnitude *= strengthMod;
weakMagnitude *= strengthMod;
// the duration doesn't matter because we are not updating the gamecontroller state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_GameControllerRumble(gamepadPtr, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;
return drivers.rumbleDriver().rumble(strongMagnitude, weakMagnitude);
}
@Override
public boolean canRumble() {
return rumbleSupported
return drivers.rumbleDriver().isRumbleSupported()
&& config().allowVibrations
&& ControlifyApi.get().currentInputMode() == InputMode.CONTROLLER;
}
@ -125,28 +117,8 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
return this.rumbleManager;
}
@Override
public void open() {
if (SDL2NativesManager.isLoaded()) {
this.gamepadPtr = SDL.SDL_GameControllerOpen(joystickId);
Controlify.LOGGER.info(SDL.SDL_GetError());
this.rumbleSupported = SDL.SDL_GameControllerHasRumble(gamepadPtr);
this.triggerRumbleSupported = SDL.SDL_GameControllerHasRumble(gamepadPtr);
if (this.hasGyro = SDL.SDL_GameControllerHasSensor(gamepadPtr, SDL.SDL_SENSOR_GYRO)) {
SDL.SDL_GameControllerSetSensorEnabled(gamepadPtr, SDL.SDL_SENSOR_GYRO, true);
}
} else {
this.gamepadPtr = 0;
this.rumbleSupported = false;
this.hasGyro = false;
}
}
@Override
public void close() {
SDL.SDL_GameControllerClose(gamepadPtr);
this.gamepadPtr = 0;
this.rumbleSupported = false;
this.hasGyro = false;
uniqueDrivers.forEach(Driver::close);
}
}

View File

@ -138,7 +138,6 @@ public final class GamepadState implements ControllerState {
"gamepadButtons=" + gamepadButtons + ']';
}
public record AxesState(
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
@ -178,23 +177,6 @@ public final class GamepadState implements ControllerState {
ControllerUtils.deadzone(rightTrigger, deadZone)
);
}
public static AxesState fromController(GamepadController controller) {
if (controller == null)
return EMPTY;
var state = controller.getGamepadState();
var axes = state.axes();
float leftX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X);
float leftY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y);
float rightX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X);
float rightY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y);
float leftTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER) + 1f) / 2f;
float rightTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) + 1f) / 2f;
return new AxesState(leftX, leftY, rightX, rightY, leftTrigger, rightTrigger);
}
}
public record ButtonState(
@ -211,32 +193,6 @@ public final class GamepadState implements ControllerState {
false, false, false, false,
false, false
);
public static ButtonState fromController(GamepadController controller) {
if (controller == null)
return EMPTY;
var state = controller.getGamepadState();
var buttons = state.buttons();
boolean a = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_A) == GLFW.GLFW_PRESS;
boolean b = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_B) == GLFW.GLFW_PRESS;
boolean x = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_X) == GLFW.GLFW_PRESS;
boolean y = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_Y) == GLFW.GLFW_PRESS;
boolean leftBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == GLFW.GLFW_PRESS;
boolean rightBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == GLFW.GLFW_PRESS;
boolean back = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_BACK) == GLFW.GLFW_PRESS;
boolean start = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_START) == GLFW.GLFW_PRESS;
boolean guide = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_GUIDE) == GLFW.GLFW_PRESS;
boolean dpadUp = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW.GLFW_PRESS;
boolean dpadDown = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW.GLFW_PRESS;
boolean dpadLeft = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW.GLFW_PRESS;
boolean dpadRight = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW.GLFW_PRESS;
boolean leftStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_THUMB) == GLFW.GLFW_PRESS;
boolean rightStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB) == GLFW.GLFW_PRESS;
return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick);
}
}
public record GyroState(float pitch, float yaw, float roll) {

View File

@ -1,5 +1,6 @@
package dev.isxander.controlify.controller.hid;
import com.mojang.datafixers.util.Pair;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.ControllerType;
import org.hid4java.*;
@ -11,7 +12,7 @@ public class ControllerHIDService {
private final HidServicesSpecification specification;
private HidServices services;
private final Queue<HIDIdentifierWithPath> unconsumedControllerHIDs;
private final Queue<Pair<HidDevice, HIDIdentifier>> unconsumedControllerHIDs;
private final Map<String, HidDevice> attachedDevices = new HashMap<>();
private boolean disabled = false;
// https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page
@ -41,19 +42,19 @@ public class ControllerHIDService {
public ControllerHIDInfo fetchType() {
doScanOnThisThread();
HIDIdentifierWithPath hid = unconsumedControllerHIDs.poll();
Pair<HidDevice, HIDIdentifier> hid = unconsumedControllerHIDs.poll();
if (hid == null) {
Controlify.LOGGER.warn("No controller found via USB hardware scan! This prevents identifying controller type.");
return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty());
}
ControllerType type = ControllerType.getTypeMap().getOrDefault(hid.identifier(), ControllerType.UNKNOWN);
ControllerType type = ControllerType.getTypeMap().getOrDefault(hid.getSecond(), ControllerType.UNKNOWN);
if (type == ControllerType.UNKNOWN)
Controlify.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.identifier());
Controlify.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.getSecond());
unconsumedControllerHIDs.removeIf(h -> 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<String> path) {
public record ControllerHIDInfo(ControllerType type, Optional<HidDevice> hidDevice) {
public Optional<String> 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) {
}
}

View File

@ -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<JoystickState,
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
this.rumbleSupported = SDL2NativesManager.isLoaded() && SDL.SDL_JoystickHasRumble(this.ptrJoystick);
this.rumbleManager = new RumbleManager(this);
this.bindings = new ControllerBindings<>(this);
}
@Override
@ -117,13 +124,6 @@ public class SingleJoystickController extends AbstractController<JoystickState,
return this.rumbleManager;
}
@Override
public void open() {
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
this.rumbleSupported = SDL2NativesManager.isLoaded() && SDL.SDL_JoystickHasRumble(this.ptrJoystick);
this.rumbleManager = new RumbleManager(this);
}
@Override
public void close() {
SDL.SDL_JoystickClose(ptrJoystick);

View File

@ -0,0 +1,23 @@
package dev.isxander.controlify.driver;
import dev.isxander.controlify.controller.gamepad.GamepadState;
public interface BasicGamepadInputDriver extends Driver {
BasicGamepadState getBasicGamepadState();
record BasicGamepadState(GamepadState.AxesState axes, GamepadState.ButtonState buttons) {
public static final BasicGamepadState EMPTY = new BasicGamepadState(GamepadState.AxesState.EMPTY, GamepadState.ButtonState.EMPTY);
}
BasicGamepadInputDriver UNSUPPORTED = new BasicGamepadInputDriver() {
@Override
public void update() {
}
@Override
public BasicGamepadState getBasicGamepadState() {
return BasicGamepadState.EMPTY;
}
};
}

View File

@ -0,0 +1,8 @@
package dev.isxander.controlify.driver;
public interface Driver {
void update();
default void close() {
}
}

View File

@ -0,0 +1,54 @@
package dev.isxander.controlify.driver;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWGamepadState;
public class GLFWGamepadDriver implements BasicGamepadInputDriver {
private final int jid;
private BasicGamepadState state = new BasicGamepadState(GamepadState.AxesState.EMPTY, GamepadState.ButtonState.EMPTY);
public GLFWGamepadDriver(int jid) {
this.jid = jid;
}
@Override
public void update() {
GLFWGamepadState state = GLFWGamepadState.create();
GLFW.glfwGetGamepadState(jid, state);
GamepadState.AxesState axes = new GamepadState.AxesState(
state.axes(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X),
state.axes(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y),
state.axes(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X),
state.axes(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y),
state.axes(GLFW.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER),
state.axes(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER)
);
GamepadState.ButtonState buttons = new GamepadState.ButtonState(
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_A) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_B) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_X) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_Y) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_BACK) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_START) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_GUIDE) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_THUMB) == GLFW.GLFW_PRESS,
state.buttons(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB) == GLFW.GLFW_PRESS
);
this.state = new BasicGamepadState(axes, buttons);
}
@Override
public BasicGamepadState getBasicGamepadState() {
return state;
}
}

View File

@ -0,0 +1,33 @@
package dev.isxander.controlify.driver;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import org.hid4java.HidDevice;
import java.util.*;
public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, GyroDriver gyroDriver, RumbleDriver rumbleDriver) {
public Set<Driver> getUniqueDrivers() {
Set<Driver> drivers = Collections.newSetFromMap(new IdentityHashMap<>());
drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver));
return drivers;
}
public static GamepadDrivers forController(int jid, Optional<HidDevice> 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);
}
}

View File

@ -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;
}
};
}

View File

@ -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;
}
};
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}