forked from Clones/Controlify
controller hid identification + ps4 buttons
This commit is contained in:
@ -1,7 +1,10 @@
|
||||
package dev.isxander.controlify.controller;
|
||||
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.bindings.ControllerTheme;
|
||||
import dev.isxander.controlify.controller.hid.HIDIdentifier;
|
||||
import dev.isxander.controlify.event.ControlifyEvents;
|
||||
import org.hid4java.HidDevice;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWGamepadState;
|
||||
|
||||
@ -11,12 +14,14 @@ import java.util.Objects;
|
||||
|
||||
public final class Controller {
|
||||
public static final Map<Integer, Controller> CONTROLLERS = new HashMap<>();
|
||||
public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false);
|
||||
public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false, "DUMMY", ControllerType.UNKNOWN);
|
||||
|
||||
private final int id;
|
||||
private final int joystickId;
|
||||
private final String guid;
|
||||
private final String name;
|
||||
private final boolean gamepad;
|
||||
private final String uid;
|
||||
private final ControllerType type;
|
||||
|
||||
private ControllerState state = ControllerState.EMPTY;
|
||||
private ControllerState prevState = ControllerState.EMPTY;
|
||||
@ -24,11 +29,13 @@ public final class Controller {
|
||||
private final ControllerBindings bindings = new ControllerBindings(this);
|
||||
private ControllerConfig config = new ControllerConfig();
|
||||
|
||||
public Controller(int id, String guid, String name, boolean gamepad) {
|
||||
this.id = id;
|
||||
public Controller(int joystickId, String guid, String name, boolean gamepad, String uid, ControllerType type) {
|
||||
this.joystickId = joystickId;
|
||||
this.guid = guid;
|
||||
this.name = name;
|
||||
this.gamepad = gamepad;
|
||||
this.uid = uid;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ControllerState state() {
|
||||
@ -63,24 +70,32 @@ public final class Controller {
|
||||
}
|
||||
|
||||
public boolean connected() {
|
||||
return GLFW.glfwJoystickPresent(id);
|
||||
return GLFW.glfwJoystickPresent(joystickId);
|
||||
}
|
||||
|
||||
GLFWGamepadState getGamepadState() {
|
||||
GLFWGamepadState state = GLFWGamepadState.create();
|
||||
if (gamepad)
|
||||
GLFW.glfwGetGamepadState(id, state);
|
||||
GLFW.glfwGetGamepadState(joystickId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
return joystickId;
|
||||
}
|
||||
|
||||
public String guid() {
|
||||
return guid;
|
||||
}
|
||||
|
||||
public String uid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public ControllerType type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
if (config().customName != null)
|
||||
return config().customName;
|
||||
@ -112,7 +127,7 @@ public final class Controller {
|
||||
return Objects.hash(guid);
|
||||
}
|
||||
|
||||
public static Controller byId(int id) {
|
||||
public static Controller create(int id, HidDevice device) {
|
||||
if (id > GLFW.GLFW_JOYSTICK_LAST)
|
||||
throw new IllegalArgumentException("Invalid joystick id: " + id);
|
||||
if (CONTROLLERS.containsKey(id))
|
||||
@ -120,10 +135,16 @@ public final class Controller {
|
||||
|
||||
String guid = GLFW.glfwGetJoystickGUID(id);
|
||||
boolean gamepad = GLFW.glfwJoystickIsGamepad(id);
|
||||
String name = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
|
||||
if (name == null) name = Integer.toString(id);
|
||||
String fallbackName = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
|
||||
String uid = device.getPath();
|
||||
ControllerType type = ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId()));
|
||||
String name = type != ControllerType.UNKNOWN || fallbackName == null ? type.friendlyName() : fallbackName;
|
||||
int tries = 1;
|
||||
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equals)) {
|
||||
name = type.friendlyName() + " (" + tries++ + ")";
|
||||
}
|
||||
|
||||
Controller controller = new Controller(id, guid, name, gamepad);
|
||||
Controller controller = new Controller(id, guid, name, gamepad, uid, type);
|
||||
CONTROLLERS.put(id, controller);
|
||||
|
||||
return controller;
|
||||
|
@ -1,5 +1,7 @@
|
||||
package dev.isxander.controlify.controller;
|
||||
|
||||
import dev.isxander.controlify.bindings.ControllerTheme;
|
||||
|
||||
public class ControllerConfig {
|
||||
public static final ControllerConfig DEFAULT = new ControllerConfig();
|
||||
|
||||
@ -20,5 +22,7 @@ public class ControllerConfig {
|
||||
|
||||
public float virtualMouseSensitivity = 1f;
|
||||
|
||||
public ControllerTheme theme = ControllerTheme.AUTO;
|
||||
|
||||
public String customName = null;
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
package dev.isxander.controlify.controller;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import dev.isxander.controlify.bindings.ControllerTheme;
|
||||
import dev.isxander.controlify.controller.hid.HIDIdentifier;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum ControllerType {
|
||||
UNKNOWN("Unknown Controller", ControllerTheme.XBOX_ONE),
|
||||
XBOX_ONE("Xbox Controller", ControllerTheme.XBOX_ONE),
|
||||
XBOX_360("Xbox 360 Controller", ControllerTheme.XBOX_ONE),
|
||||
DUALSHOCK4("PS4 Controller", ControllerTheme.DUALSHOCK4);
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setLenient().create();
|
||||
private static Map<HIDIdentifier, ControllerType> typeMap = null;
|
||||
|
||||
private final String friendlyName;
|
||||
private final ControllerTheme theme;
|
||||
|
||||
ControllerType(String friendlyName, ControllerTheme theme) {
|
||||
this.friendlyName = friendlyName;
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
public String friendlyName() {
|
||||
return friendlyName;
|
||||
}
|
||||
|
||||
public ControllerTheme theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public static ControllerType getTypeForHID(HIDIdentifier hid) {
|
||||
if (typeMap != null) return typeMap.getOrDefault(hid, UNKNOWN);
|
||||
|
||||
typeMap = new HashMap<>();
|
||||
try {
|
||||
try (var hidDb = ControllerType.class.getResourceAsStream("/hiddb.json5")) {
|
||||
var json = GSON.fromJson(new InputStreamReader(hidDb), JsonObject.class);
|
||||
for (var type : ControllerType.values()) {
|
||||
if (!json.has(type.name().toLowerCase())) continue;
|
||||
|
||||
var themeJson = json.getAsJsonObject(type.name().toLowerCase());
|
||||
|
||||
int vendorId = themeJson.get("vendor").getAsInt();
|
||||
for (var productIdEntry : themeJson.getAsJsonArray("product")) {
|
||||
int productId = productIdEntry.getAsInt();
|
||||
typeMap.put(new HIDIdentifier(vendorId, productId), type);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return typeMap.getOrDefault(hid, UNKNOWN);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package dev.isxander.controlify.controller.hid;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import org.hid4java.*;
|
||||
import org.hid4java.event.HidServicesEvent;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ControllerHIDService implements HidServicesListener {
|
||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page
|
||||
private static final Set<Integer> CONTROLLER_USAGE_IDS = Set.of(
|
||||
0x04, // Joystick
|
||||
0x05, // Gamepad
|
||||
0x08 // Multi-axis Controller
|
||||
);
|
||||
|
||||
private final HidServicesSpecification specification;
|
||||
private final Queue<Consumer<HidDevice>> deviceQueue;
|
||||
|
||||
public ControllerHIDService() {
|
||||
this.deviceQueue = new ArrayDeque<>();
|
||||
|
||||
this.specification = new HidServicesSpecification();
|
||||
specification.setAutoStart(false);
|
||||
specification.setScanInterval(2000); // long interval, so we can guarantee this runs after GLFW hook
|
||||
}
|
||||
|
||||
public void start() {
|
||||
var services = HidManager.getHidServices(specification);
|
||||
services.addHidServicesListener(this);
|
||||
|
||||
services.start();
|
||||
}
|
||||
|
||||
public void awaitNextDevice(Consumer<HidDevice> consumer) {
|
||||
deviceQueue.add(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceAttached(HidServicesEvent event) {
|
||||
var device = event.getHidDevice();
|
||||
|
||||
if (isController(device)) {
|
||||
if (deviceQueue.peek() != null) {
|
||||
deviceQueue.poll().accept(event.getHidDevice());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isController(HidDevice device) {
|
||||
var isGenericDesktopControlOrGameControl = device.getUsagePage() == 0x1 || device.getUsagePage() == 0x5;
|
||||
var isController = CONTROLLER_USAGE_IDS.contains(device.getUsage());
|
||||
return isGenericDesktopControlOrGameControl && isController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceDetached(HidServicesEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidFailure(HidServicesEvent event) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package dev.isxander.controlify.controller.hid;
|
||||
|
||||
public record HIDIdentifier(int vendorId, int productId) {
|
||||
}
|
Reference in New Issue
Block a user