controller hid identification + ps4 buttons
@ -47,6 +47,13 @@ dependencies {
|
|||||||
implementation(libs.mixin.extras)
|
implementation(libs.mixin.extras)
|
||||||
annotationProcessor(libs.mixin.extras)
|
annotationProcessor(libs.mixin.extras)
|
||||||
include(libs.mixin.extras)
|
include(libs.mixin.extras)
|
||||||
|
|
||||||
|
implementation(libs.hid4java)
|
||||||
|
include(libs.hid4java)
|
||||||
|
}
|
||||||
|
|
||||||
|
machete {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
@ -14,6 +14,7 @@ fabric_api = "0.73.3+1.19.4"
|
|||||||
mixin_extras = "0.2.0-beta.1"
|
mixin_extras = "0.2.0-beta.1"
|
||||||
yet_another_config_lib = "2.2.0+update.1.19.4-SNAPSHOT"
|
yet_another_config_lib = "2.2.0+update.1.19.4-SNAPSHOT"
|
||||||
mod_menu = "6.0.0-beta.1"
|
mod_menu = "6.0.0-beta.1"
|
||||||
|
hid4java = "0.7.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
|
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
|
||||||
@ -23,6 +24,7 @@ fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fab
|
|||||||
mixin_extras = { module = "com.github.llamalad7:mixinextras", version.ref = "mixin_extras" }
|
mixin_extras = { module = "com.github.llamalad7:mixinextras", version.ref = "mixin_extras" }
|
||||||
yet_another_config_lib = { module = "dev.isxander:yet-another-config-lib", version.ref = "yet_another_config_lib" }
|
yet_another_config_lib = { module = "dev.isxander:yet-another-config-lib", version.ref = "yet_another_config_lib" }
|
||||||
mod_menu = { module = "com.terraformersmc:modmenu", version.ref = "mod_menu" }
|
mod_menu = { module = "com.terraformersmc:modmenu", version.ref = "mod_menu" }
|
||||||
|
hid4java = { module = "org.hid4java:hid4java", version.ref = "hid4java" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
loom = { id = "fabric-loom", version.ref = "loom" }
|
loom = { id = "fabric-loom", version.ref = "loom" }
|
||||||
|
@ -5,6 +5,7 @@ import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider;
|
|||||||
import dev.isxander.controlify.config.ControlifyConfig;
|
import dev.isxander.controlify.config.ControlifyConfig;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.ControllerState;
|
import dev.isxander.controlify.controller.ControllerState;
|
||||||
|
import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
||||||
import dev.isxander.controlify.event.ControlifyEvents;
|
import dev.isxander.controlify.event.ControlifyEvents;
|
||||||
import dev.isxander.controlify.ingame.InGameInputHandler;
|
import dev.isxander.controlify.ingame.InGameInputHandler;
|
||||||
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
|
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
|
||||||
@ -24,51 +25,65 @@ public class Controlify {
|
|||||||
private InGameInputHandler inGameInputHandler;
|
private InGameInputHandler inGameInputHandler;
|
||||||
private VirtualMouseHandler virtualMouseHandler;
|
private VirtualMouseHandler virtualMouseHandler;
|
||||||
private InputMode currentInputMode;
|
private InputMode currentInputMode;
|
||||||
|
private ControllerHIDService controllerHIDService;
|
||||||
|
|
||||||
private final ControlifyConfig config = new ControlifyConfig();
|
private final ControlifyConfig config = new ControlifyConfig();
|
||||||
|
|
||||||
public void onInitializeInput() {
|
public void onInitializeInput() {
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
|
inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controllers
|
||||||
|
controllerHIDService = new ControllerHIDService();
|
||||||
|
|
||||||
// find already connected controllers
|
// find already connected controllers
|
||||||
for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) {
|
for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) {
|
||||||
if (GLFW.glfwJoystickPresent(i)) {
|
if (GLFW.glfwJoystickPresent(i)) {
|
||||||
setCurrentController(Controller.byId(i));
|
int jid = i;
|
||||||
LOGGER.info("Controller found: " + currentController.name());
|
controllerHIDService.awaitNextDevice(device -> {
|
||||||
|
setCurrentController(Controller.create(jid, device));
|
||||||
|
LOGGER.info("Controller found: " + currentController.name());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controllerHIDService.start();
|
||||||
|
|
||||||
// load after initial controller discovery
|
// load after initial controller discovery
|
||||||
config().load();
|
config().load();
|
||||||
|
|
||||||
// listen for new controllers
|
// listen for new controllers
|
||||||
GLFW.glfwSetJoystickCallback((jid, event) -> {
|
GLFW.glfwSetJoystickCallback((jid, event) -> {
|
||||||
if (event == GLFW.GLFW_CONNECTED) {
|
if (event == GLFW.GLFW_CONNECTED) {
|
||||||
setCurrentController(Controller.byId(jid));
|
controllerHIDService.awaitNextDevice(device -> {
|
||||||
LOGGER.info("Controller connected: " + currentController.name());
|
setCurrentController(Controller.create(jid, device));
|
||||||
this.setCurrentInputMode(InputMode.CONTROLLER);
|
LOGGER.info("Controller connected: " + currentController.name() + " (" + device.getPath() + ")");
|
||||||
|
this.setCurrentInputMode(InputMode.CONTROLLER);
|
||||||
|
|
||||||
config().load(); // load config again if a configuration already exists for this controller
|
config().load(); // load config again if a configuration already exists for this controller
|
||||||
config().save(); // save config if it doesn't exist
|
config().save(); // save config if it doesn't exist
|
||||||
|
|
||||||
|
minecraft.getToasts().addToast(SystemToast.multiline(
|
||||||
|
minecraft,
|
||||||
|
SystemToast.SystemToastIds.PERIODIC_NOTIFICATION,
|
||||||
|
Component.translatable("controlify.toast.controller_connected.title"),
|
||||||
|
Component.translatable("controlify.toast.controller_connected.description")
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
minecraft.getToasts().addToast(SystemToast.multiline(
|
|
||||||
minecraft,
|
|
||||||
SystemToast.SystemToastIds.PERIODIC_NOTIFICATION,
|
|
||||||
Component.translatable("controlify.toast.controller_connected.title"),
|
|
||||||
Component.translatable("controlify.toast.controller_connected.description")
|
|
||||||
));
|
|
||||||
} else if (event == GLFW.GLFW_DISCONNECTED) {
|
} else if (event == GLFW.GLFW_DISCONNECTED) {
|
||||||
var controller = Controller.CONTROLLERS.remove(jid);
|
var controller = Controller.CONTROLLERS.remove(jid);
|
||||||
setCurrentController(Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null));
|
if (controller != null) {
|
||||||
LOGGER.info("Controller disconnected: " + controller.name());
|
setCurrentController(Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null));
|
||||||
this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
|
LOGGER.info("Controller disconnected: " + controller.name());
|
||||||
|
this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
|
||||||
|
|
||||||
minecraft.getToasts().addToast(SystemToast.multiline(
|
minecraft.getToasts().addToast(SystemToast.multiline(
|
||||||
minecraft,
|
minecraft,
|
||||||
SystemToast.SystemToastIds.PERIODIC_NOTIFICATION,
|
SystemToast.SystemToastIds.PERIODIC_NOTIFICATION,
|
||||||
Component.translatable("controlify.toast.controller_disconnected.title"),
|
Component.translatable("controlify.toast.controller_disconnected.title"),
|
||||||
Component.translatable("controlify.toast.controller_disconnected.description", controller.name())
|
Component.translatable("controlify.toast.controller_disconnected.description", controller.name())
|
||||||
));
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,12 +28,10 @@ public enum Bind {
|
|||||||
|
|
||||||
private final BiFunction<ControllerState, Controller, Boolean> state;
|
private final BiFunction<ControllerState, Controller, Boolean> state;
|
||||||
private final String identifier;
|
private final String identifier;
|
||||||
private final ResourceLocation textureLocation;
|
|
||||||
|
|
||||||
Bind(BiFunction<ControllerState, Controller, Boolean> state, String identifier) {
|
Bind(BiFunction<ControllerState, Controller, Boolean> state, String identifier) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
this.textureLocation = new ResourceLocation("controlify", "textures/gui/buttons/xbox/" + identifier + ".png");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bind(Function<ControllerState, Boolean> state, String identifier) {
|
Bind(Function<ControllerState, Boolean> state, String identifier) {
|
||||||
@ -48,8 +46,8 @@ public enum Bind {
|
|||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceLocation textureLocation() {
|
public ResourceLocation textureLocation(Controller controller) {
|
||||||
return textureLocation;
|
return new ResourceLocation("controlify", "textures/gui/buttons/" + controller.config().theme.id(controller) + "/" + identifier + ".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bind fromIdentifier(String identifier) {
|
public static Bind fromIdentifier(String identifier) {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package dev.isxander.controlify.bindings;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.yacl.api.NameableEnum;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public enum ControllerTheme implements NameableEnum {
|
||||||
|
AUTO(c -> c.type().theme().id(c)),
|
||||||
|
XBOX_ONE(c -> "xbox"),
|
||||||
|
DUALSHOCK4(c -> "dualshock4");
|
||||||
|
|
||||||
|
private final Function<Controller, String> idGetter;
|
||||||
|
|
||||||
|
ControllerTheme(Function<Controller, String> idGetter) {
|
||||||
|
this.idGetter = idGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id(Controller controller) {
|
||||||
|
return idGetter.apply(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getDisplayName() {
|
||||||
|
return Component.translatable("controlify.controller_theme." + name().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,7 @@ public class ControlifyConfig {
|
|||||||
for (var controller : Controller.CONTROLLERS.values()) {
|
for (var controller : Controller.CONTROLLERS.values()) {
|
||||||
// `add` replaces if already existing
|
// `add` replaces if already existing
|
||||||
// TODO: find a better way to identify controllers, GUID will report the same for multiple controllers of the same model
|
// TODO: find a better way to identify controllers, GUID will report the same for multiple controllers of the same model
|
||||||
newControllerData.add(controller.guid(), generateControllerConfig(controller));
|
newControllerData.add(controller.uid(), generateControllerConfig(controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
controllerData = newControllerData;
|
controllerData = newControllerData;
|
||||||
@ -84,7 +84,7 @@ public class ControlifyConfig {
|
|||||||
JsonObject controllers = object.getAsJsonObject("controllers");
|
JsonObject controllers = object.getAsJsonObject("controllers");
|
||||||
if (controllers != null) {
|
if (controllers != null) {
|
||||||
for (var controller : Controller.CONTROLLERS.values()) {
|
for (var controller : Controller.CONTROLLERS.values()) {
|
||||||
var settings = controllers.getAsJsonObject(controller.guid());
|
var settings = controllers.getAsJsonObject(controller.uid());
|
||||||
if (settings != null) {
|
if (settings != null) {
|
||||||
applyControllerConfig(controller, settings);
|
applyControllerConfig(controller, settings);
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ import org.lwjgl.glfw.GLFW;
|
|||||||
|
|
||||||
public class BindButtonController implements Controller<Bind> {
|
public class BindButtonController implements Controller<Bind> {
|
||||||
private final Option<Bind> option;
|
private final Option<Bind> option;
|
||||||
|
private final dev.isxander.controlify.controller.Controller controller;
|
||||||
|
|
||||||
public BindButtonController(Option<Bind> option) {
|
public BindButtonController(Option<Bind> option, dev.isxander.controlify.controller.Controller controller) {
|
||||||
this.option = option;
|
this.option = option;
|
||||||
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -52,7 +54,7 @@ public class BindButtonController implements Controller<Bind> {
|
|||||||
if (awaitingControllerInput) {
|
if (awaitingControllerInput) {
|
||||||
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
|
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
|
||||||
} else {
|
} else {
|
||||||
ButtonRenderer.drawButton(control.option().pendingValue(), matrices, getDimension().xLimit() - ButtonRenderer.BUTTON_SIZE / 2, getDimension().centerY(), ButtonRenderer.BUTTON_SIZE);
|
ButtonRenderer.drawButton(control.option().pendingValue(), control.controller, matrices, getDimension().xLimit() - ButtonRenderer.BUTTON_SIZE / 2, getDimension().centerY(), ButtonRenderer.BUTTON_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@ package dev.isxander.controlify.config.gui;
|
|||||||
|
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.bindings.Bind;
|
import dev.isxander.controlify.bindings.Bind;
|
||||||
|
import dev.isxander.controlify.bindings.ControllerTheme;
|
||||||
import dev.isxander.controlify.config.GlobalSettings;
|
import dev.isxander.controlify.config.GlobalSettings;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.ControllerConfig;
|
import dev.isxander.controlify.controller.ControllerConfig;
|
||||||
import dev.isxander.yacl.api.*;
|
import dev.isxander.yacl.api.*;
|
||||||
import dev.isxander.yacl.gui.controllers.cycling.CyclingListController;
|
import dev.isxander.yacl.gui.controllers.cycling.CyclingListController;
|
||||||
|
import dev.isxander.yacl.gui.controllers.cycling.EnumController;
|
||||||
import dev.isxander.yacl.gui.controllers.slider.FloatSliderController;
|
import dev.isxander.yacl.gui.controllers.slider.FloatSliderController;
|
||||||
import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController;
|
import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController;
|
||||||
import dev.isxander.yacl.gui.controllers.string.StringController;
|
import dev.isxander.yacl.gui.controllers.string.StringController;
|
||||||
@ -102,6 +104,13 @@ public class YACLHelper {
|
|||||||
.tooltip(Component.translatable("controlify.gui.right_trigger_threshold.tooltip"))
|
.tooltip(Component.translatable("controlify.gui.right_trigger_threshold.tooltip"))
|
||||||
.binding(def.rightTriggerActivationThreshold, () -> config.rightTriggerActivationThreshold, v -> config.rightTriggerActivationThreshold = v)
|
.binding(def.rightTriggerActivationThreshold, () -> config.rightTriggerActivationThreshold, v -> config.rightTriggerActivationThreshold = v)
|
||||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
||||||
|
.build())
|
||||||
|
.option(Option.createBuilder(ControllerTheme.class)
|
||||||
|
.name(Component.translatable("controlify.gui.controller_theme"))
|
||||||
|
.tooltip(Component.translatable("controlify.gui.controller_theme.tooltip"))
|
||||||
|
.binding(def.theme, () -> config.theme, v -> config.theme = v)
|
||||||
|
.controller(EnumController::new)
|
||||||
|
.instant(true)
|
||||||
.build());
|
.build());
|
||||||
category.group(configGroup.build());
|
category.group(configGroup.build());
|
||||||
|
|
||||||
@ -111,7 +120,7 @@ public class YACLHelper {
|
|||||||
controlsGroup.option(Option.createBuilder(Bind.class)
|
controlsGroup.option(Option.createBuilder(Bind.class)
|
||||||
.name(control.name())
|
.name(control.name())
|
||||||
.binding(control.defaultBind(), control::currentBind, control::setCurrentBind)
|
.binding(control.defaultBind(), control::currentBind, control::setCurrentBind)
|
||||||
.controller(BindButtonController::new)
|
.controller(opt -> new BindButtonController(opt, controller))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
category.group(controlsGroup.build());
|
category.group(controlsGroup.build());
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package dev.isxander.controlify.controller;
|
package dev.isxander.controlify.controller;
|
||||||
|
|
||||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
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 dev.isxander.controlify.event.ControlifyEvents;
|
||||||
|
import org.hid4java.HidDevice;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.lwjgl.glfw.GLFWGamepadState;
|
import org.lwjgl.glfw.GLFWGamepadState;
|
||||||
|
|
||||||
@ -11,12 +14,14 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public final class Controller {
|
public final class Controller {
|
||||||
public static final Map<Integer, Controller> CONTROLLERS = new HashMap<>();
|
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 guid;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final boolean gamepad;
|
private final boolean gamepad;
|
||||||
|
private final String uid;
|
||||||
|
private final ControllerType type;
|
||||||
|
|
||||||
private ControllerState state = ControllerState.EMPTY;
|
private ControllerState state = ControllerState.EMPTY;
|
||||||
private ControllerState prevState = ControllerState.EMPTY;
|
private ControllerState prevState = ControllerState.EMPTY;
|
||||||
@ -24,11 +29,13 @@ public final class Controller {
|
|||||||
private final ControllerBindings bindings = new ControllerBindings(this);
|
private final ControllerBindings bindings = new ControllerBindings(this);
|
||||||
private ControllerConfig config = new ControllerConfig();
|
private ControllerConfig config = new ControllerConfig();
|
||||||
|
|
||||||
public Controller(int id, String guid, String name, boolean gamepad) {
|
public Controller(int joystickId, String guid, String name, boolean gamepad, String uid, ControllerType type) {
|
||||||
this.id = id;
|
this.joystickId = joystickId;
|
||||||
this.guid = guid;
|
this.guid = guid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.gamepad = gamepad;
|
this.gamepad = gamepad;
|
||||||
|
this.uid = uid;
|
||||||
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ControllerState state() {
|
public ControllerState state() {
|
||||||
@ -63,24 +70,32 @@ public final class Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean connected() {
|
public boolean connected() {
|
||||||
return GLFW.glfwJoystickPresent(id);
|
return GLFW.glfwJoystickPresent(joystickId);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLFWGamepadState getGamepadState() {
|
GLFWGamepadState getGamepadState() {
|
||||||
GLFWGamepadState state = GLFWGamepadState.create();
|
GLFWGamepadState state = GLFWGamepadState.create();
|
||||||
if (gamepad)
|
if (gamepad)
|
||||||
GLFW.glfwGetGamepadState(id, state);
|
GLFW.glfwGetGamepadState(joystickId, state);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int id() {
|
public int id() {
|
||||||
return id;
|
return joystickId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String guid() {
|
public String guid() {
|
||||||
return guid;
|
return guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String uid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerType type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
public String name() {
|
public String name() {
|
||||||
if (config().customName != null)
|
if (config().customName != null)
|
||||||
return config().customName;
|
return config().customName;
|
||||||
@ -112,7 +127,7 @@ public final class Controller {
|
|||||||
return Objects.hash(guid);
|
return Objects.hash(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Controller byId(int id) {
|
public static Controller create(int id, HidDevice device) {
|
||||||
if (id > GLFW.GLFW_JOYSTICK_LAST)
|
if (id > GLFW.GLFW_JOYSTICK_LAST)
|
||||||
throw new IllegalArgumentException("Invalid joystick id: " + id);
|
throw new IllegalArgumentException("Invalid joystick id: " + id);
|
||||||
if (CONTROLLERS.containsKey(id))
|
if (CONTROLLERS.containsKey(id))
|
||||||
@ -120,10 +135,16 @@ public final class Controller {
|
|||||||
|
|
||||||
String guid = GLFW.glfwGetJoystickGUID(id);
|
String guid = GLFW.glfwGetJoystickGUID(id);
|
||||||
boolean gamepad = GLFW.glfwJoystickIsGamepad(id);
|
boolean gamepad = GLFW.glfwJoystickIsGamepad(id);
|
||||||
String name = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
|
String fallbackName = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
|
||||||
if (name == null) name = Integer.toString(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);
|
CONTROLLERS.put(id, controller);
|
||||||
|
|
||||||
return controller;
|
return controller;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package dev.isxander.controlify.controller;
|
package dev.isxander.controlify.controller;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.bindings.ControllerTheme;
|
||||||
|
|
||||||
public class ControllerConfig {
|
public class ControllerConfig {
|
||||||
public static final ControllerConfig DEFAULT = new ControllerConfig();
|
public static final ControllerConfig DEFAULT = new ControllerConfig();
|
||||||
|
|
||||||
@ -20,5 +22,7 @@ public class ControllerConfig {
|
|||||||
|
|
||||||
public float virtualMouseSensitivity = 1f;
|
public float virtualMouseSensitivity = 1f;
|
||||||
|
|
||||||
|
public ControllerTheme theme = ControllerTheme.AUTO;
|
||||||
|
|
||||||
public String customName = null;
|
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) {
|
||||||
|
}
|
@ -3,13 +3,14 @@ package dev.isxander.controlify.gui;
|
|||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
import com.mojang.blaze3d.vertex.PoseStack;
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
import dev.isxander.controlify.bindings.Bind;
|
import dev.isxander.controlify.bindings.Bind;
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import net.minecraft.client.gui.GuiComponent;
|
import net.minecraft.client.gui.GuiComponent;
|
||||||
|
|
||||||
public class ButtonRenderer {
|
public class ButtonRenderer {
|
||||||
public static final int BUTTON_SIZE = 22;
|
public static final int BUTTON_SIZE = 22;
|
||||||
|
|
||||||
public static void drawButton(Bind button, PoseStack poseStack, int x, int y, int size) {
|
public static void drawButton(Bind button, Controller controller, PoseStack poseStack, int x, int y, int size) {
|
||||||
RenderSystem.setShaderTexture(0, button.textureLocation());
|
RenderSystem.setShaderTexture(0, button.textureLocation(controller));
|
||||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||||
|
|
||||||
GuiComponent.blit(poseStack, x - size / 2, y - size / 2, 0, 0, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE);
|
GuiComponent.blit(poseStack, x - size / 2, y - size / 2, 0, 0, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE);
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
"controlify.gui.left_trigger_threshold.tooltip": "How far the left trigger needs to be pushed before registering as pressed.",
|
"controlify.gui.left_trigger_threshold.tooltip": "How far the left trigger needs to be pushed before registering as pressed.",
|
||||||
"controlify.gui.right_trigger_threshold": "Right Trigger Threshold",
|
"controlify.gui.right_trigger_threshold": "Right Trigger Threshold",
|
||||||
"controlify.gui.right_trigger_threshold.tooltip": "How far the right trigger needs to be pushed before registering as pressed.",
|
"controlify.gui.right_trigger_threshold.tooltip": "How far the right trigger needs to be pushed before registering as pressed.",
|
||||||
|
"controlify.gui.controller_theme": "Controller Theme",
|
||||||
|
"controlify.gui.controller_theme.tooltip": "The theme to use for rendering controller buttons.",
|
||||||
|
|
||||||
"controlify.gui.group.controls": "Controls",
|
"controlify.gui.group.controls": "Controls",
|
||||||
"controlify.gui.group.controls.tooltip": "Adjust the controller controls.",
|
"controlify.gui.group.controls.tooltip": "Adjust the controller controls.",
|
||||||
@ -43,6 +45,10 @@
|
|||||||
"controlify.toast.controller_disconnected.title": "Controller Disconnected",
|
"controlify.toast.controller_disconnected.title": "Controller Disconnected",
|
||||||
"controlify.toast.controller_disconnected.description": "'%s' was disconnected.",
|
"controlify.toast.controller_disconnected.description": "'%s' was disconnected.",
|
||||||
|
|
||||||
|
"controlify.controller_theme.auto": "Auto",
|
||||||
|
"controlify.controller_theme.xbox_one": "Xbox",
|
||||||
|
"controlify.controller_theme.dualshock4": "PS4",
|
||||||
|
|
||||||
"controlify.binding.controlify.jump": "Jump",
|
"controlify.binding.controlify.jump": "Jump",
|
||||||
"controlify.binding.controlify.sneak": "Sneak",
|
"controlify.binding.controlify.sneak": "Sneak",
|
||||||
"controlify.binding.controlify.attack": "Attack",
|
"controlify.binding.controlify.attack": "Attack",
|
||||||
|
Before Width: | Height: | Size: 224 B |
Before Width: | Height: | Size: 238 B |
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 232 B |
Before Width: | Height: | Size: 253 B |
Before Width: | Height: | Size: 310 B |
Before Width: | Height: | Size: 294 B |
Before Width: | Height: | Size: 320 B |
Before Width: | Height: | Size: 302 B |
Before Width: | Height: | Size: 257 B |
Before Width: | Height: | Size: 271 B |
Before Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 380 B |
Before Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 282 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 220 B |
Before Width: | Height: | Size: 269 B |
Before Width: | Height: | Size: 246 B |
Before Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 264 B |
Before Width: | Height: | Size: 1.5 KiB |
30
src/main/resources/hiddb.json5
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// THIS FILE IS PARSED BY LENIENT GSON PARSER AND IS NOT JSON5 COMPLIANT!
|
||||||
|
{
|
||||||
|
"xbox_one": {
|
||||||
|
"vendor": 1118, // 0x45e
|
||||||
|
"friendly_name": "Xbox One Controller",
|
||||||
|
"product": [
|
||||||
|
767, // 0x2ff
|
||||||
|
746, // 0x2ea
|
||||||
|
2834, // 0xb12
|
||||||
|
733, // 0x2dd
|
||||||
|
739, // 0x2e3
|
||||||
|
742, // 0x2e6
|
||||||
|
765, // 0x2fd
|
||||||
|
721, // 0x2d1
|
||||||
|
649, // 0x289
|
||||||
|
514, // 0x202
|
||||||
|
645, // 0x285
|
||||||
|
648 // 0x288
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dualshock4": {
|
||||||
|
"vendor": 1356, // 0x54c
|
||||||
|
"friendly_name": "PS4 Controller",
|
||||||
|
"product": [
|
||||||
|
1476, // 0x5c4
|
||||||
|
2508, // 0x9cc
|
||||||
|
2976 // 0xba0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|