diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerType.java b/src/main/java/dev/isxander/controlify/controller/ControllerType.java index b67ac4e..2cfb255 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerType.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerType.java @@ -47,8 +47,7 @@ public record ControllerType(String friendlyName, String identifier) { while (reader.hasNext()) { String friendlyName = null; String identifier = null; - int vendorId = -1; - Set productIds = new HashSet<>(); + Set hids = new HashSet<>(); reader.beginObject(); while (reader.hasNext()) { @@ -57,11 +56,24 @@ public record ControllerType(String friendlyName, String identifier) { switch (name) { case "name" -> friendlyName = reader.nextString(); case "identifier" -> identifier = reader.nextString(); - case "vendor" -> vendorId = reader.nextInt(); - case "product" -> { + case "hids" -> { reader.beginArray(); while (reader.hasNext()) { - productIds.add(reader.nextInt()); + int vendorId = -1; + int productId = -1; + reader.beginArray(); + while (reader.hasNext()) { + if (vendorId == -1) { + vendorId = reader.nextInt(); + } else if (productId == -1) { + productId = reader.nextInt(); + } else { + Controlify.LOGGER.warn("Too many values in HID array. Skipping..."); + reader.skipValue(); + } + } + reader.endArray(); + hids.add(new HIDIdentifier(vendorId, productId)); } reader.endArray(); } @@ -73,14 +85,14 @@ public record ControllerType(String friendlyName, String identifier) { } reader.endObject(); - if (friendlyName == null || identifier == null || vendorId == -1 || productIds.isEmpty()) { + if (friendlyName == null || identifier == null || hids.isEmpty()) { Controlify.LOGGER.warn("Invalid entry in HID DB. Skipping..."); continue; } var type = new ControllerType(friendlyName, identifier); - for (int productId : productIds) { - typeMap.put(new HIDIdentifier(vendorId, productId), type); + for (var hid : hids) { + typeMap.put(hid, type); } } reader.endArray(); diff --git a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java index e955718..4b9a6cf 100644 --- a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java +++ b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java @@ -3,28 +3,34 @@ package dev.isxander.controlify.controller.hid; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.ControllerType; import org.hid4java.*; -import org.hid4java.event.HidServicesEvent; import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; -public class ControllerHIDService implements HidServicesListener { +public class ControllerHIDService { private final HidServicesSpecification specification; private HidServices services; - private final Map unconsumedHIDs; + private final Queue unconsumedControllerHIDs; + private final Map attachedDevices = new HashMap<>(); private boolean disabled = false; + // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page + private static final Set CONTROLLER_USAGE_IDS = Set.of( + 0x04, // Joystick + 0x05, // Gamepad + 0x08 // Multi-axis Controller + ); public ControllerHIDService() { this.specification = new HidServicesSpecification(); specification.setAutoStart(false); - this.unconsumedHIDs = new LinkedHashMap<>(); + specification.setScanMode(ScanMode.NO_SCAN); + this.unconsumedControllerHIDs = new ArrayBlockingQueue<>(50); } public void start() { try { services = HidManager.getHidServices(specification); - services.addHidServicesListener(this); - services.start(); } catch (HidException e) { Controlify.LOGGER.error("Failed to start controller HID service! If you are on Linux using flatpak or snap, this is likely because your launcher has not added libusb to their package.", e); @@ -33,48 +39,74 @@ public class ControllerHIDService implements HidServicesListener { } public ControllerHIDInfo fetchType() { - services.scan(); - try { - // wait for scan to complete on separate thread - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); + doScanOnThisThread(); + + HIDIdentifierWithPath 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()); } - var typeMap = ControllerType.getTypeMap(); - for (var entry : unconsumedHIDs.entrySet()) { - var path = entry.getKey(); - var hid = entry.getValue(); - var type = typeMap.get(hid); - if (type != null) { - Controlify.LOGGER.info("identified controller type " + type); - unconsumedHIDs.remove(path); - return new ControllerHIDInfo(type, Optional.of(path)); - } - } + ControllerType type = ControllerType.getTypeMap().getOrDefault(hid.identifier(), 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 type unknown! Please report the make and model of your controller and give the following details: " + unconsumedHIDs); - return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); - } + unconsumedControllerHIDs.removeIf(h -> hid.path().equals(h.path())); - @Override - public void hidDeviceAttached(HidServicesEvent event) { - var device = event.getHidDevice(); - unconsumedHIDs.put(device.getPath(), new HIDIdentifier(device.getVendorId(), device.getProductId())); + return new ControllerHIDInfo(type, Optional.of(hid.path())); } public boolean isDisabled() { return disabled; } - @Override - public void hidDeviceDetached(HidServicesEvent event) { - unconsumedHIDs.remove(event.getHidDevice().getPath()); + private void doScanOnThisThread() { + List removeList = new ArrayList(); + + List attachedHidDeviceList = services.getAttachedHidDevices(); + + for (HidDevice attachedDevice : attachedHidDeviceList) { + + if (!this.attachedDevices.containsKey(attachedDevice.getId())) { + + // Device has become attached so add it but do not open + attachedDevices.put(attachedDevice.getId(), attachedDevice); + + // 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)); + } + } + + for (Map.Entry entry : attachedDevices.entrySet()) { + + String deviceId = entry.getKey(); + HidDevice hidDevice = entry.getValue(); + + if (!attachedHidDeviceList.contains(hidDevice)) { + + // Keep track of removals + removeList.add(deviceId); + + // remove device from unconsumed list + unconsumedControllerHIDs.removeIf(device -> this.attachedDevices.get(deviceId).getPath().equals(device.path())); + } + } + + if (!removeList.isEmpty()) { + // Update the attached devices map + removeList.forEach(this.attachedDevices.keySet()::remove); + } } - @Override - public void hidFailure(HidServicesEvent event) { + private boolean isController(HidDevice device) { + boolean isControllerType = ControllerType.getTypeMap().containsKey(new HIDIdentifier(device.getVendorId(), device.getProductId())); + 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); } public record ControllerHIDInfo(ControllerType type, Optional path) { @@ -82,4 +114,7 @@ public class ControllerHIDService implements HidServicesListener { return path.map(p -> UUID.nameUUIDFromBytes(p.getBytes())).map(UUID::toString); } } + + private record HIDIdentifierWithPath(String path, HIDIdentifier identifier) { + } } diff --git a/src/main/resources/assets/controlify/controllers/controller_identification.json5 b/src/main/resources/assets/controlify/controllers/controller_identification.json5 index e47d88a..058148b 100644 --- a/src/main/resources/assets/controlify/controllers/controller_identification.json5 +++ b/src/main/resources/assets/controlify/controllers/controller_identification.json5 @@ -3,50 +3,46 @@ "name": "Xbox One Controller", "identifier": "xbox_one", - "vendor": 0x45e, - "product": [ - 0x2ff, - 0x2ea, - 0xb12, - 0x2dd, - 0x2e3, - 0x2e6, - 0x2fd, - 0x2d1, - 0x289, - 0x202, - 0x285, - 0x288, - 0xb13, + "hids": [ + [0x45e, 0x2ff], + [0x45e, 0x2ea], + [0x45e, 0xb12], + [0x45e, 0x2dd], + [0x45e, 0x2e6], + [0x45e, 0x2fd], + [0x45e, 0x2e3], + [0x45e, 0x2d1], + [0x45e, 0x289], + [0x45e, 0x202], + [0x45e, 0x285], + [0x45e, 0x288], + [0x45e, 0xb13], ] }, { "name": "Dualshock 4 Controller", "identifier": "dualshock4", - "vendor": 0x54c, - "product": [ - 0x5c4, - 0x9cc, - 0xba0, + "hids": [ + [0x54c, 0x5c4], + [0x54c, 0x9cc], + [0x54c, 0xba0], ] }, { "name": "Steam Deck", "identifier": "steam_deck", - "vendor": 0x28de, - "product": [ - 0x1205, + "hids": [ + [0x28de, 0x1205], ] }, { "name": "Stadia Controller", "identifier": "stadia", - "vendor": 0x18d1, - "product": [ - 0x9400, + "hids": [ + [0x18d1, 0x9400], ] } ] diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/unknown b/src/main/resources/assets/controlify/textures/gui/gamepad/unknown new file mode 120000 index 0000000..905d028 --- /dev/null +++ b/src/main/resources/assets/controlify/textures/gui/gamepad/unknown @@ -0,0 +1 @@ +xbox_one \ No newline at end of file