1
0
forked from Clones/Controlify

joystick support

This commit is contained in:
isXander
2023-02-16 12:25:55 +00:00
parent 1b5c9daf94
commit 5a1504df76
134 changed files with 2296 additions and 820 deletions

View File

@ -1,8 +1,10 @@
package dev.isxander.controlify.config.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.Bind;
import dev.isxander.controlify.bindings.GamepadBind;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
@ -16,20 +18,17 @@ import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.LinkedHashSet;
import java.util.Set;
public class GamepadBindController implements Controller<IBind<GamepadState>> {
private final Option<IBind<GamepadState>> option;
private final GamepadController controller;
public class BindButtonController implements Controller<IBind> {
private final Option<IBind> option;
private final dev.isxander.controlify.controller.Controller controller;
public BindButtonController(Option<IBind> option, dev.isxander.controlify.controller.Controller controller) {
public GamepadBindController(Option<IBind<GamepadState>> option, GamepadController controller) {
this.option = option;
this.controller = controller;
}
@Override
public Option<IBind> option() {
public Option<IBind<GamepadState>> option() {
return this.option;
}
@ -43,26 +42,18 @@ public class BindButtonController implements Controller<IBind> {
return new BindButtonWidget(this, yaclScreen, dimension);
}
public static class BindButtonWidget extends ControllerWidget<BindButtonController> implements ComponentProcessorProvider, ComponentProcessor {
public static class BindButtonWidget extends ControllerWidget<GamepadBindController> implements ComponentProcessorProvider, ComponentProcessor {
private boolean awaitingControllerInput = false;
private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC);
private final Set<Bind> pressedBinds = new LinkedHashSet<>();
public BindButtonWidget(BindButtonController control, YACLScreen screen, Dimension<Integer> dim) {
public BindButtonWidget(GamepadBindController control, YACLScreen screen, Dimension<Integer> dim) {
super(control, screen, dim);
}
@Override
protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) {
if (awaitingControllerInput) {
if (pressedBinds.isEmpty()) {
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
} else {
var bind = IBind.create(pressedBinds);
var plusSize = 2 + textRenderer.width("+");
bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width() - getXPadding() - plusSize, getDimension().centerY(), control.controller);
textRenderer.drawShadow(matrices, "+", getDimension().xLimit() - getXPadding() - plusSize, getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
}
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
} else {
var bind = control.option().pendingValue();
bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width(), getDimension().centerY(), control.controller);
@ -95,41 +86,31 @@ public class BindButtonController implements Controller<IBind> {
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
if (controller != control.controller) return true;
if (controller.bindings().GUI_PRESS.justPressed() && !awaitingControllerInput) {
return awaitingControllerInput = true;
}
if (!awaitingControllerInput) return false;
if (pressedBinds.stream().anyMatch(bind -> !bind.held(controller.state(), controller))) {
// finished
awaitingControllerInput = false;
control.option().requestSet(IBind.create(pressedBinds));
pressedBinds.clear();
} else {
for (var bind : Bind.values()) {
if (bind.held(controller.state(), controller) && !bind.held(controller.prevState(), controller)) {
if (bind == Bind.GUIDE) { // FIXME: guide cannot be used as reserve because Windows hooks into xbox button to open game bar, maybe START?
if (pressedBinds.isEmpty()) {
awaitingControllerInput = false;
control.option().requestSet(IBind.create(Bind.NONE));
pressedBinds.clear();
return true;
}
} else {
pressedBinds.add(bind);
}
}
var gamepad = control.controller;
for (var bind : GamepadBind.values()) {
if (bind.held(gamepad.state(), gamepad) && !bind.held(gamepad.prevState(), gamepad)) {
control.option().requestSet(bind);
awaitingControllerInput = false;
gamepad.consumeButtonState();
return true;
}
control.controller.consumeButtonState();
}
return true;
return false;
}
@Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller controller) {
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
return awaitingControllerInput;
}

View File

@ -0,0 +1,164 @@
package dev.isxander.controlify.config.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.*;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.yacl.api.Controller;
import dev.isxander.yacl.api.Option;
import dev.isxander.yacl.api.utils.Dimension;
import dev.isxander.yacl.gui.AbstractWidget;
import dev.isxander.yacl.gui.YACLScreen;
import dev.isxander.yacl.gui.controllers.ControllerWidget;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
public class JoystickBindController implements Controller<IBind<JoystickState>> {
private final Option<IBind<JoystickState>> option;
private final JoystickController controller;
public JoystickBindController(Option<IBind<JoystickState>> option, JoystickController controller) {
this.option = option;
this.controller = controller;
}
@Override
public Option<IBind<JoystickState>> option() {
return this.option;
}
@Override
public Component formatValue() {
return Component.empty();
}
@Override
public AbstractWidget provideWidget(YACLScreen yaclScreen, Dimension<Integer> dimension) {
return new BindButtonWidget(this, yaclScreen, dimension);
}
public static class BindButtonWidget extends ControllerWidget<JoystickBindController> implements ComponentProcessorProvider, ComponentProcessor {
private boolean awaitingControllerInput = false;
private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC);
public BindButtonWidget(JoystickBindController control, YACLScreen screen, Dimension<Integer> dim) {
super(control, screen, dim);
}
@Override
protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) {
if (awaitingControllerInput) {
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
} else {
var bind = control.option().pendingValue();
bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width(), getDimension().centerY(), control.controller);
}
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (isFocused() && keyCode == GLFW.GLFW_KEY_ENTER && !awaitingControllerInput) {
awaitingControllerInput = true;
return true;
}
return false;
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (getDimension().isPointInside((int)mouseX, (int)mouseY)) {
awaitingControllerInput = true;
return true;
}
return false;
}
@Override
public ComponentProcessor componentProcessor() {
return this;
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
if (controller != control.controller) return true;
if (controller.bindings().GUI_PRESS.justPressed() && !awaitingControllerInput) {
return awaitingControllerInput = true;
}
if (!awaitingControllerInput) return false;
var joystick = control.controller;
var state = joystick.state();
var prevState = joystick.prevState();
for (int i = 0; i < Math.min(state.buttons().size(), prevState.buttons().size()); i++) {
if (state.buttons().get(i) && !prevState.buttons().get(i)) {
control.option().requestSet(new JoystickButtonBind(joystick, i));
awaitingControllerInput = false;
return true;
}
}
for (int i = 0; i < Math.min(state.axes().size(), prevState.axes().size()); i++) {
var axis = state.axes().get(i);
var prevAxis = prevState.axes().get(i);
var activationThreshold = joystick.config().buttonActivationThreshold;
if (Math.abs(prevAxis) < activationThreshold) {
if (axis > activationThreshold) {
control.option().requestSet(new JoystickAxisBind(joystick, i, JoystickAxisBind.AxisDirection.POSITIVE));
awaitingControllerInput = false;
return true;
} else if (axis < -activationThreshold) {
control.option().requestSet(new JoystickAxisBind(joystick, i, JoystickAxisBind.AxisDirection.NEGATIVE));
awaitingControllerInput = false;
return true;
}
}
}
for (int i = 0; i < Math.min(state.hats().size(), prevState.hats().size()); i++) {
var hat = state.hats().get(i);
var prevHat = prevState.hats().get(i);
if (prevHat.isCentered() && !hat.isCentered()) {
control.option().requestSet(new JoystickHatBind(joystick, i, hat));
awaitingControllerInput = false;
return true;
}
}
return false;
}
@Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
return awaitingControllerInput;
}
@Override
protected int getHoveredControlWidth() {
return getUnhoveredControlWidth();
}
@Override
protected int getUnhoveredControlWidth() {
if (awaitingControllerInput)
return textRenderer.width(awaitingText);
return control.option().pendingValue().drawSize().width();
}
}
}

View File

@ -3,8 +3,12 @@ package dev.isxander.controlify.config.gui;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.config.GlobalSettings;
import dev.isxander.controlify.controller.ControllerTheme;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.yacl.api.*;
import dev.isxander.yacl.gui.controllers.ActionController;
@ -22,6 +26,10 @@ import net.minecraft.client.gui.screens.AlertScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class YACLHelper {
public static Screen generateConfigScreen(Screen parent) {
if (Controlify.instance().currentController() == null) {
@ -41,11 +49,11 @@ public class YACLHelper {
var globalSettings = Controlify.instance().config().globalSettings();
var globalCategory = ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.category.global"))
.option(Option.createBuilder(Controller.class)
.option(Option.createBuilder((Class<Controller<?, ?>>) (Class<?>) Controller.class)
.name(Component.translatable("controlify.gui.current_controller"))
.tooltip(Component.translatable("controlify.gui.current_controller.tooltip"))
.binding(Controlify.instance().currentController(), () -> Controlify.instance().currentController(), v -> Controlify.instance().setCurrentController(v))
.controller(opt -> new CyclingListController<>(opt, Controller.CONTROLLERS.values().stream().filter(Controller::connected).toList(), c -> Component.literal(c.name())))
.controller(opt -> new CyclingListController<>(opt, Controller.CONTROLLERS.values(), c -> Component.literal(c.name())))
.instant(true)
.build())
.option(Option.createBuilder(boolean.class)
@ -114,14 +122,22 @@ public class YACLHelper {
.tooltip(Component.translatable("controlify.gui.vmouse_sensitivity.tooltip"))
.binding(def.virtualMouseSensitivity, () -> config.virtualMouseSensitivity, v -> config.virtualMouseSensitivity = v)
.controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 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(controller.type().theme(), () -> config.theme, v -> config.theme = v)
.controller(EnumController::new)
.instant(true)
.build())
.build());
if (controller instanceof GamepadController gamepad) {
var gamepadConfig = gamepad.config();
var defaultGamepadConfig = gamepad.defaultConfig();
basicGroup.option(Option.createBuilder(BuiltinGamepadTheme.class)
.name(Component.translatable("controlify.gui.controller_theme"))
.tooltip(Component.translatable("controlify.gui.controller_theme.tooltip"))
.binding(defaultGamepadConfig.theme, () -> gamepadConfig.theme, v -> gamepadConfig.theme = v)
.controller(EnumController::new)
.instant(true)
.build());
}
basicGroup
.option(Option.createBuilder(String.class)
.name(Component.translatable("controlify.gui.custom_name"))
.tooltip(Component.translatable("controlify.gui.custom_name.tooltip"))
@ -139,21 +155,59 @@ public class YACLHelper {
.tooltip(Component.translatable("controlify.gui.screen_repeat_navi_delay.tooltip"))
.binding(def.screenRepeatNavigationDelay, () -> config.screenRepeatNavigationDelay, v -> config.screenRepeatNavigationDelay = v)
.controller(opt -> new IntegerSliderController(opt, 1, 20, 1, v -> Component.translatable("controlify.gui.format.ticks", v)))
.build())
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.left_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip"))
.build());
if (controller instanceof GamepadController gamepad) {
var gpCfg = gamepad.config();
var gpCfgDef = gamepad.defaultConfig();
advancedGroup
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.left_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
)
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build())
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.right_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
)
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build());
} else if (controller instanceof JoystickController joystick) {
Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount())
.filter(i -> joystick.mapping().axis(i).requiresDeadzone())
.boxed()
.collect(Collectors.toMap(
i -> joystick.mapping().axis(i).identifier(),
i -> i,
(x, y) -> x
))
.values();
var jsCfg = joystick.config();
var jsCfgDef = joystick.defaultConfig();
for (int i : deadzoneAxes) {
advancedGroup.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", joystick.mapping().axis(i).name()))
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", joystick.mapping().axis(i).name()))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(def.leftStickDeadzone, () -> config.leftStickDeadzone, v -> config.leftStickDeadzone = v)
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build())
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.right_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(def.rightStickDeadzone, () -> config.rightStickDeadzone, v -> config.rightStickDeadzone = v)
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build())
.build());
}
}
advancedGroup
.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.auto_calibration"))
.tooltip(Component.translatable("controlify.gui.auto_calibration.tooltip"))
@ -170,19 +224,26 @@ public class YACLHelper {
var controlsGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.controls"));
for (var control : controller.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder(IBind.class)
.name(control.name())
.binding(control.defaultBind(), control::currentBind, control::setCurrentBind)
.controller(opt -> new BindButtonController(opt, controller))
.tooltip(control.description())
.instant(true)
.listener((opt, bind) -> { // yacl instant options have a bug where they don't save
opt.applyValue();
controlify.config().save();
})
.build());
if (controller instanceof GamepadController gamepad) {
for (var binding : gamepad.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder((Class<IBind<GamepadState>>) (Class<?>) IBind.class)
.name(binding.name())
.binding(binding.defaultBind(), binding::currentBind, binding::setCurrentBind)
.controller(opt -> new GamepadBindController(opt, gamepad))
.tooltip(binding.description())
.build());
}
} else if (controller instanceof JoystickController joystick) {
for (var binding : joystick.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder((Class<IBind<JoystickState>>) (Class<?>) IBind.class)
.name(binding.name())
.binding(binding.defaultBind(), binding::currentBind, binding::setCurrentBind)
.controller(opt -> new JoystickBindController(opt, joystick))
.tooltip(binding.description())
.build());
}
}
category.group(controlsGroup.build());
yacl.category(category.build());