From 6eaf16d8c37b40dd0b5e2042b1cd18d6e842b0bb Mon Sep 17 00:00:00 2001 From: isXander Date: Sun, 16 Apr 2023 16:38:44 +0100 Subject: [PATCH] better navigation --- gradle/libs.versions.toml | 2 +- .../dev/isxander/controlify/Controlify.java | 26 +- ...erControllerElementComponentProcessor.java | 35 ++- .../controlify/config/ControlifyConfig.java | 4 +- .../controlify/config/gui/YACLHelper.java | 278 +++++++++--------- .../controlify/controller/Controller.java | 8 + .../controller/ControllerConfig.java | 2 - .../gamepad/BuiltinGamepadTheme.java | 19 +- .../controlify/screenop/ScreenProcessor.java | 22 +- .../AbstractSliderComponentProcessor.java | 35 ++- .../controlify/utils/NavigationHelper.java | 35 +++ 11 files changed, 277 insertions(+), 189 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/utils/NavigationHelper.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 377c7e9..2c1fa53 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ quilt_mappings = "10" fabric_loader = "0.14.17" fabric_api = "0.78.0+1.19.4" mixin_extras = "0.2.0-beta.6" -yet_another_config_lib = "2.4.0" +yet_another_config_lib = "2.4.1" mod_menu = "6.1.0-rc.4" hid4java = "0.7.0" quilt_json5 = "1.0.3" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 222104f..d092817 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -47,7 +47,7 @@ public class Controlify implements ControlifyApi { private final Minecraft minecraft = Minecraft.getInstance(); - private Controller currentController = Controller.DUMMY; + private Controller currentController = null; private InGameInputHandler inGameInputHandler; public InGameButtonGuide inGameButtonGuide; private VirtualMouseHandler virtualMouseHandler; @@ -113,7 +113,7 @@ public class Controlify implements ControlifyApi { LOGGER.info("No controllers found."); } - if (currentController() == Controller.DUMMY && config().isFirstLaunch()) { + if (getCurrentController().isEmpty() && config().isFirstLaunch()) { this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); } else { // setCurrentController saves config @@ -156,7 +156,7 @@ public class Controlify implements ControlifyApi { ResourcePackActivationType.DEFAULT_ENABLED ); - this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller + this.inGameInputHandler = null; this.virtualMouseHandler = new VirtualMouseHandler(); controllerHIDService = new ControllerHIDService(); @@ -206,7 +206,13 @@ public class Controlify implements ControlifyApi { } } - wrapControllerError(() -> tickController(currentController, outOfFocus), "Ticking current controller", currentController); + getCurrentController().ifPresent(currentController -> { + wrapControllerError( + () -> tickController(currentController, outOfFocus), + "Ticking current controller", + currentController + ); + }); } private void tickController(Controller controller, boolean outOfFocus) { @@ -269,7 +275,7 @@ public class Controlify implements ControlifyApi { LOGGER.info("Controller connected: " + controller.name()); - config().loadOrCreateControllerData(currentController); + config().loadOrCreateControllerData(controller); if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { controller.config().allowVibrations = false; @@ -374,7 +380,7 @@ public class Controlify implements ControlifyApi { this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player); } - if (!controller.config().calibrated && controller != Controller.DUMMY) + if (!controller.config().calibrated) calibrationQueue.add(controller); } @@ -410,12 +416,12 @@ public class Controlify implements ControlifyApi { ScreenProcessorProvider.provide(minecraft.screen).onInputModeChanged(currentInputMode); } if (Minecraft.getInstance().player != null) { - if (currentInputMode == InputMode.KEYBOARD_MOUSE) + if (currentInputMode == InputMode.KEYBOARD_MOUSE) { this.inGameButtonGuide = null; - else - this.inGameButtonGuide = new InGameButtonGuide(this.currentController != null ? currentController : Controller.DUMMY, Minecraft.getInstance().player); + } else { + this.inGameButtonGuide = this.getCurrentController().map(c -> new InGameButtonGuide(c, Minecraft.getInstance().player)).orElse(null); + } } - if (Blaze3D.getTime() - lastInputSwitchTime < 20) { consecutiveInputSwitches++; } else { diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/SliderControllerElementComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/SliderControllerElementComponentProcessor.java index a100f15..52a63ad 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/yacl/SliderControllerElementComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/SliderControllerElementComponentProcessor.java @@ -3,12 +3,12 @@ package dev.isxander.controlify.compatibility.yacl; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.utils.NavigationHelper; import dev.isxander.yacl.gui.controllers.slider.SliderControllerElement; public class SliderControllerElementComponentProcessor implements ComponentProcessor { private final SliderControllerElement slider; - private int ticksSinceIncrement = 0; - private boolean prevLeft, prevRight; + private final NavigationHelper navigationHelper = new NavigationHelper(15, 3); public SliderControllerElementComponentProcessor(SliderControllerElement element) { this.slider = element; @@ -16,24 +16,29 @@ public class SliderControllerElementComponentProcessor implements ComponentProce @Override public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { - ticksSinceIncrement++; - var left = controller.bindings().CYCLE_OPT_BACKWARD.held(); + var leftPrev = controller.bindings().CYCLE_OPT_BACKWARD.prevHeld(); var right = controller.bindings().CYCLE_OPT_FORWARD.held(); + var rightPrev = controller.bindings().CYCLE_OPT_FORWARD.prevHeld(); - if (left || right) { - if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || left != prevLeft || right != prevRight) { - slider.incrementValue(left ? -1 : 1); - ticksSinceIncrement = 0; - prevLeft = left; - prevRight = right; - return true; - } + boolean repeatEventAvailable = navigationHelper.canNavigate(); + + if (left && (repeatEventAvailable || !leftPrev)) { + slider.incrementValue(-1); + + if (!leftPrev) + navigationHelper.reset(); + } else if (right && (repeatEventAvailable || !rightPrev)) { + slider.incrementValue(1); + + if (!rightPrev) + navigationHelper.reset(); } else { - this.prevLeft = false; - this.prevRight = false; + return false; } - return false; + navigationHelper.onNavigate(); + + return true; } } diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index c18d61a..a750141 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -83,7 +83,7 @@ public class ControlifyConfig { } controllerData = newControllerData; - config.addProperty("current_controller", currentControllerUid = controlify.currentController().uid()); + config.addProperty("current_controller", currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null)); config.add("controllers", controllerData); config.add("compound_joysticks", GSON.toJsonTree(compoundJoysticks.values().toArray(new CompoundJoystickInfo[0]))); config.add("global", GSON.toJsonTree(globalSettings)); @@ -127,7 +127,7 @@ public class ControlifyConfig { if (object.has("current_controller")) { currentControllerUid = object.get("current_controller").getAsString(); } else { - currentControllerUid = controlify.currentController().uid(); + currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null); setDirty(); } } diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index d63cbcc..cc90283 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -1,12 +1,14 @@ package dev.isxander.controlify.config.gui; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.api.bind.ControllerBinding; import dev.isxander.controlify.bindings.ControllerBindingImpl; import dev.isxander.controlify.bindings.IBind; import dev.isxander.controlify.config.GlobalSettings; import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerConfig; import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadState; @@ -42,6 +44,9 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; public class YACLHelper { + private static final Function percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); + private static final Function percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v); + public static Screen generateConfigScreen(Screen parent) { var controlify = Controlify.instance(); @@ -57,7 +62,7 @@ public class YACLHelper { .option(Option.createBuilder((Class>) (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)) + .binding(Controlify.instance().getCurrentController().orElse(Controller.DUMMY), () -> Controlify.instance().getCurrentController().orElse(Controller.DUMMY), v -> Controlify.instance().setCurrentController(v)) .controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), Controller.CONTROLLERS.values().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name()))) .build()) .option(globalVibrationOption = Option.createBuilder(boolean.class) @@ -119,9 +124,6 @@ public class YACLHelper { var config = controller.config(); var def = controller.defaultConfig(); - Function percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); - Function percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v); - var basicGroup = OptionGroup.createBuilder() .name(Component.translatable("controlify.gui.group.basic")) .tooltip(Component.translatable("controlify.gui.group.basic.tooltip")) @@ -208,145 +210,20 @@ public class YACLHelper { .build()); category.group(basicGroup.build()); - var vibrationGroup = OptionGroup.createBuilder() - .name(Component.translatable("controlify.gui.group.vibration")) - .tooltip(Component.translatable("controlify.gui.group.vibration.tooltip")); - List> strengthOptions = new ArrayList<>(); - Option allowVibrationOption; - vibrationGroup.option(allowVibrationOption = Option.createBuilder(boolean.class) - .name(Component.translatable("controlify.gui.allow_vibrations")) - .tooltip(Component.translatable("controlify.gui.allow_vibrations.tooltip")) - .binding(globalVibrationOption.pendingValue(), () -> config.allowVibrations && globalVibrationOption.pendingValue(), v -> config.allowVibrations = v) - .available(globalVibrationOption.pendingValue()) - .listener((opt, allowVibration) -> strengthOptions.forEach(so -> so.setAvailable(allowVibration))) - .controller(TickBoxController::new) - .build()); - for (RumbleSource source : RumbleSource.values()) { - var option = Option.createBuilder(float.class) - .name(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath())) - .tooltip(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath() + ".tooltip")) - .binding( - def.getRumbleStrength(source), - () -> config.getRumbleStrength(source), - v -> config.setRumbleStrength(source, v) - ) - .controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter)) - .available(allowVibrationOption.pendingValue()) - .build(); - strengthOptions.add(option); - vibrationGroup.option(option); + if (controller.canRumble()) { + category.group(makeVibrationGroup(globalVibrationOption, config, def)); } - category.group(vibrationGroup.build()); if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) { - var gpCfg = gamepad.config(); - var gpCfgDef = gamepad.defaultConfig(); - - Option gyroSensitivity; - List> gyroOptions = new ArrayList<>(); - var gyroGroup = OptionGroup.createBuilder() - .name(Component.translatable("controlify.gui.group.gyro")) - .tooltip(Component.translatable("controlify.gui.group.gyro.tooltip")) - .option(gyroSensitivity = Option.createBuilder(float.class) - .name(Component.translatable("controlify.gui.gyro_look_sensitivity")) - .tooltip(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip")) - .binding(gpCfgDef.gyroLookSensitivity, () -> gpCfg.gyroLookSensitivity, v -> gpCfg.gyroLookSensitivity = v) - .controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter)) - .listener((opt, sensitivity) -> gyroOptions.forEach(o -> { - o.setAvailable(sensitivity > 0); - o.requestSetDefault(); - })) - .build()) - .option(Util.make(() -> { - var opt = Option.createBuilder(boolean.class) - .name(Component.translatable("controlify.gui.gyro_requires_button")) - .tooltip(Component.translatable("controlify.gui.gyro_requires_button.tooltip")) - .binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v) - .controller(TickBoxController::new) - .available(gyroSensitivity.pendingValue() > 0) - .build(); - gyroOptions.add(opt); - return opt; - })) - .option(Util.make(() -> { - var opt = Option.createBuilder(boolean.class) - .name(Component.translatable("controlify.gui.flick_stick")) - .tooltip(Component.translatable("controlify.gui.flick_stick.tooltip")) - .binding(gpCfgDef.flickStick, () -> gpCfg.flickStick, v -> gpCfg.flickStick = v) - .controller(TickBoxController::new) - .available(gyroSensitivity.pendingValue() > 0) - .build(); - gyroOptions.add(opt); - return opt; - })); - - category.group(gyroGroup.build()); + category.group(makeGyroGroup(gamepad)); } var advancedGroup = OptionGroup.createBuilder() .name(Component.translatable("controlify.gui.group.advanced")) .tooltip(Component.translatable("controlify.gui.group.advanced.tooltip")) - .collapsed(true) - .option(Option.createBuilder(int.class) - .name(Component.translatable("controlify.gui.screen_repeat_navi_delay")) - .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()); + .collapsed(true); - if (controller instanceof GamepadController gamepad) { - var gpCfg = gamepad.config(); - var gpCfgDef = gamepad.defaultConfig(); - advancedGroup - .option(Option.createBuilder(float.class) - .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick"))) - .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick"))) - .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.axis_deadzone", Component.translatable("controlify.gui.right_stick"))) - .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick"))) - .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 SingleJoystickController joystick) { - JoystickMapping.Axis[] axes = joystick.mapping().axes(); - Collection deadzoneAxes = IntStream.range(0, axes.length) - .filter(i -> axes[i].requiresDeadzone()) - .boxed() - .collect(Collectors.toMap( - i -> axes[i].identifier(), - i -> i, - (x, y) -> x, - LinkedHashMap::new - )) - .values(); - var jsCfg = joystick.config(); - var jsCfgDef = joystick.defaultConfig(); - - for (int i : deadzoneAxes) { - var axis = axes[i]; - - advancedGroup.option(Option.createBuilder(float.class) - .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name())) - .tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name())) - .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) - .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()); - } - } + addDeadzoneOptions(controller, advancedGroup); advancedGroup .option(ButtonOption.createBuilder() @@ -394,6 +271,139 @@ public class YACLHelper { return category.build(); } + private static OptionGroup makeVibrationGroup(Option globalVibrationOption, ControllerConfig config, ControllerConfig def) { + var vibrationGroup = OptionGroup.createBuilder() + .name(Component.translatable("controlify.gui.group.vibration")) + .tooltip(Component.translatable("controlify.gui.group.vibration.tooltip")); + List> strengthOptions = new ArrayList<>(); + Option allowVibrationOption; + vibrationGroup.option(allowVibrationOption = Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.allow_vibrations")) + .tooltip(Component.translatable("controlify.gui.allow_vibrations.tooltip")) + .binding(globalVibrationOption.pendingValue(), () -> config.allowVibrations && globalVibrationOption.pendingValue(), v -> config.allowVibrations = v) + .available(globalVibrationOption.pendingValue()) + .listener((opt, allowVibration) -> strengthOptions.forEach(so -> so.setAvailable(allowVibration))) + .controller(TickBoxController::new) + .build()); + for (RumbleSource source : RumbleSource.values()) { + var option = Option.createBuilder(float.class) + .name(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath())) + .tooltip(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath() + ".tooltip")) + .binding( + def.getRumbleStrength(source), + () -> config.getRumbleStrength(source), + v -> config.setRumbleStrength(source, v) + ) + .controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter)) + .available(allowVibrationOption.pendingValue()) + .build(); + strengthOptions.add(option); + vibrationGroup.option(option); + } + return vibrationGroup.build(); + } + + private static OptionGroup makeGyroGroup(GamepadController gamepad) { + var gpCfg = gamepad.config(); + var gpCfgDef = gamepad.defaultConfig(); + + Option gyroSensitivity; + List> gyroOptions = new ArrayList<>(); + var gyroGroup = OptionGroup.createBuilder() + .name(Component.translatable("controlify.gui.group.gyro")) + .tooltip(Component.translatable("controlify.gui.group.gyro.tooltip")) + .option(gyroSensitivity = Option.createBuilder(float.class) + .name(Component.translatable("controlify.gui.gyro_look_sensitivity")) + .tooltip(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip")) + .binding(gpCfgDef.gyroLookSensitivity, () -> gpCfg.gyroLookSensitivity, v -> gpCfg.gyroLookSensitivity = v) + .controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter)) + .listener((opt, sensitivity) -> gyroOptions.forEach(o -> { + o.setAvailable(sensitivity > 0); + o.requestSetDefault(); + })) + .build()) + .option(Util.make(() -> { + var opt = Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.gyro_requires_button")) + .tooltip(Component.translatable("controlify.gui.gyro_requires_button.tooltip")) + .binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v) + .controller(TickBoxController::new) + .available(gyroSensitivity.pendingValue() > 0) + .build(); + gyroOptions.add(opt); + return opt; + })) + .option(Util.make(() -> { + var opt = Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.flick_stick")) + .tooltip(Component.translatable("controlify.gui.flick_stick.tooltip")) + .binding(gpCfgDef.flickStick, () -> gpCfg.flickStick, v -> gpCfg.flickStick = v) + .controller(TickBoxController::new) + .available(gyroSensitivity.pendingValue() > 0) + .build(); + gyroOptions.add(opt); + return opt; + })); + + return gyroGroup.build(); + } + + private static void addDeadzoneOptions(Controller controller, OptionGroup.Builder group) { + if (controller instanceof GamepadController gamepad) { + var gpCfg = gamepad.config(); + var gpCfgDef = gamepad.defaultConfig(); + group + .option(Option.createBuilder(float.class) + .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick"))) + .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick"))) + .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.axis_deadzone", Component.translatable("controlify.gui.right_stick"))) + .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick"))) + .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 SingleJoystickController joystick) { + JoystickMapping.Axis[] axes = joystick.mapping().axes(); + Collection deadzoneAxes = IntStream.range(0, axes.length) + .filter(i -> axes[i].requiresDeadzone()) + .boxed() + .collect(Collectors.toMap( + i -> axes[i].identifier(), + i -> i, + (x, y) -> x, + LinkedHashMap::new + )) + .values(); + var jsCfg = joystick.config(); + var jsCfgDef = joystick.defaultConfig(); + + for (int i : deadzoneAxes) { + var axis = axes[i]; + + group.option(Option.createBuilder(float.class) + .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name())) + .tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name())) + .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) + .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()); + } + } + } + private static Map> groupBindings(Collection bindings) { return bindings.stream() .collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList())); diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index 31517f3..afa15ed 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -44,7 +44,9 @@ public interface Controller DUMMY = new Controller<>() { private final ControllerBindings bindings = new ControllerBindings<>(this); private final RumbleManager rumbleManager = new RumbleManager(new RumbleCapable() { @@ -182,5 +185,10 @@ public interface Controller { public final T screen; - protected int lastMoved = 0; + protected final NavigationHelper navigationHelper = new NavigationHelper(10, 3); protected final Minecraft minecraft = Minecraft.getInstance(); public ScreenProcessor(T screen) { @@ -66,26 +67,39 @@ public class ScreenProcessor { var accessor = (ScreenAccessor) screen; - boolean repeatEventAvailable = ++lastMoved >= controller.config().screenRepeatNavigationDelay; + boolean repeatEventAvailable = navigationHelper.canNavigate(); var bindings = controller.bindings(); FocusNavigationEvent.ArrowNavigation event = null; if (bindings.GUI_NAVI_RIGHT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_RIGHT.prevHeld())) { event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT); + + if (!bindings.GUI_NAVI_RIGHT.prevHeld()) + navigationHelper.reset(); } else if (bindings.GUI_NAVI_LEFT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_LEFT.prevHeld())) { event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT); + + if (!bindings.GUI_NAVI_LEFT.prevHeld()) + navigationHelper.reset(); } else if (bindings.GUI_NAVI_UP.held() && (repeatEventAvailable || !bindings.GUI_NAVI_UP.prevHeld())) { event = accessor.invokeCreateArrowEvent(ScreenDirection.UP); + + if (!bindings.GUI_NAVI_UP.prevHeld()) + navigationHelper.reset(); } else if (bindings.GUI_NAVI_DOWN.held() && (repeatEventAvailable || !bindings.GUI_NAVI_DOWN.prevHeld())) { event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN); + + if (!bindings.GUI_NAVI_DOWN.prevHeld()) + navigationHelper.reset(); } if (event != null) { ComponentPath path = screen.nextFocusPath(event); if (path != null) { accessor.invokeChangeFocus(path); - lastMoved = 0; + + navigationHelper.onNavigate(); var newFocusTree = getFocusTree(); while (!newFocusTree.isEmpty() && !focuses.contains(newFocusTree.peek())) { @@ -156,7 +170,7 @@ public class ScreenProcessor { ComponentPath path = screen.nextFocusPath(accessor.invokeCreateArrowEvent(ScreenDirection.DOWN)); if (path != null) { accessor.invokeChangeFocus(path); - lastMoved = 0; + navigationHelper.clearDelay(); } } } diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java index 422b1fb..1415cb4 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java @@ -3,36 +3,41 @@ package dev.isxander.controlify.screenop.compat; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.utils.NavigationHelper; /** * A component processor that handles incrementing and decrementing a slider. * This uses {@link dev.isxander.controlify.bindings.ControllerBindings#CYCLE_OPT_FORWARD} and {@link dev.isxander.controlify.bindings.ControllerBindings#CYCLE_OPT_BACKWARD} to increment and decrement the slider. */ public abstract class AbstractSliderComponentProcessor implements ComponentProcessor { - private int ticksSinceIncrement = 0; - private boolean prevLeft, prevRight; + private final NavigationHelper navigationHelper = new NavigationHelper(15, 3); @Override public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { - ticksSinceIncrement++; - var left = controller.bindings().CYCLE_OPT_BACKWARD.held(); + var leftPrev = controller.bindings().CYCLE_OPT_BACKWARD.prevHeld(); var right = controller.bindings().CYCLE_OPT_FORWARD.held(); + var rightPrev = controller.bindings().CYCLE_OPT_FORWARD.prevHeld(); - if (left || right) { - if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || left != prevLeft || right != prevRight) { - incrementSlider(left); - ticksSinceIncrement = 0; - prevLeft = left; - prevRight = right; - return true; - } + boolean repeatEventAvailable = navigationHelper.canNavigate(); + + if (left && (repeatEventAvailable || !leftPrev)) { + incrementSlider(true); + + if (!leftPrev) + navigationHelper.reset(); + } else if (right && (repeatEventAvailable || !rightPrev)) { + incrementSlider(false); + + if (!rightPrev) + navigationHelper.reset(); } else { - this.prevLeft = false; - this.prevRight = false; + return false; } - return false; + navigationHelper.onNavigate(); + + return true; } protected abstract void incrementSlider(boolean reverse); diff --git a/src/main/java/dev/isxander/controlify/utils/NavigationHelper.java b/src/main/java/dev/isxander/controlify/utils/NavigationHelper.java new file mode 100644 index 0000000..0eea20d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/NavigationHelper.java @@ -0,0 +1,35 @@ +package dev.isxander.controlify.utils; + +public class NavigationHelper { + private final int initialDelay, repeatDelay; + private int currentDelay; + + private boolean hasResetThisTick = false; + + public NavigationHelper(int initialDelay, int repeatDelay) { + this.initialDelay = initialDelay; + this.repeatDelay = repeatDelay; + this.currentDelay = 0; + } + + public boolean canNavigate() { + return --currentDelay <= 0; + } + + public void reset() { + currentDelay = initialDelay; + hasResetThisTick = true; + } + + public void clearDelay() { + currentDelay = 0; + } + + public void onNavigate() { + if (!hasResetThisTick) { + currentDelay = repeatDelay; + } else { + hasResetThisTick = false; + } + } +}