1
0
forked from Clones/Controlify

better navigation

This commit is contained in:
isXander
2023-04-16 16:38:44 +01:00
parent 8eb8510590
commit 6eaf16d8c3
11 changed files with 277 additions and 189 deletions

View File

@ -13,7 +13,7 @@ quilt_mappings = "10"
fabric_loader = "0.14.17" fabric_loader = "0.14.17"
fabric_api = "0.78.0+1.19.4" fabric_api = "0.78.0+1.19.4"
mixin_extras = "0.2.0-beta.6" 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" mod_menu = "6.1.0-rc.4"
hid4java = "0.7.0" hid4java = "0.7.0"
quilt_json5 = "1.0.3" quilt_json5 = "1.0.3"

View File

@ -47,7 +47,7 @@ public class Controlify implements ControlifyApi {
private final Minecraft minecraft = Minecraft.getInstance(); private final Minecraft minecraft = Minecraft.getInstance();
private Controller<?, ?> currentController = Controller.DUMMY; private Controller<?, ?> currentController = null;
private InGameInputHandler inGameInputHandler; private InGameInputHandler inGameInputHandler;
public InGameButtonGuide inGameButtonGuide; public InGameButtonGuide inGameButtonGuide;
private VirtualMouseHandler virtualMouseHandler; private VirtualMouseHandler virtualMouseHandler;
@ -113,7 +113,7 @@ public class Controlify implements ControlifyApi {
LOGGER.info("No controllers found."); 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)); this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
} else { } else {
// setCurrentController saves config // setCurrentController saves config
@ -156,7 +156,7 @@ public class Controlify implements ControlifyApi {
ResourcePackActivationType.DEFAULT_ENABLED 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(); this.virtualMouseHandler = new VirtualMouseHandler();
controllerHIDService = new ControllerHIDService(); 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) { private void tickController(Controller<?, ?> controller, boolean outOfFocus) {
@ -269,7 +275,7 @@ public class Controlify implements ControlifyApi {
LOGGER.info("Controller connected: " + controller.name()); LOGGER.info("Controller connected: " + controller.name());
config().loadOrCreateControllerData(currentController); config().loadOrCreateControllerData(controller);
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false; controller.config().allowVibrations = false;
@ -374,7 +380,7 @@ public class Controlify implements ControlifyApi {
this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player); this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player);
} }
if (!controller.config().calibrated && controller != Controller.DUMMY) if (!controller.config().calibrated)
calibrationQueue.add(controller); calibrationQueue.add(controller);
} }
@ -410,12 +416,12 @@ public class Controlify implements ControlifyApi {
ScreenProcessorProvider.provide(minecraft.screen).onInputModeChanged(currentInputMode); ScreenProcessorProvider.provide(minecraft.screen).onInputModeChanged(currentInputMode);
} }
if (Minecraft.getInstance().player != null) { if (Minecraft.getInstance().player != null) {
if (currentInputMode == InputMode.KEYBOARD_MOUSE) if (currentInputMode == InputMode.KEYBOARD_MOUSE) {
this.inGameButtonGuide = null; this.inGameButtonGuide = null;
else } else {
this.inGameButtonGuide = new InGameButtonGuide(this.currentController != null ? currentController : Controller.DUMMY, Minecraft.getInstance().player); this.inGameButtonGuide = this.getCurrentController().map(c -> new InGameButtonGuide(c, Minecraft.getInstance().player)).orElse(null);
}
} }
if (Blaze3D.getTime() - lastInputSwitchTime < 20) { if (Blaze3D.getTime() - lastInputSwitchTime < 20) {
consecutiveInputSwitches++; consecutiveInputSwitches++;
} else { } else {

View File

@ -3,12 +3,12 @@ package dev.isxander.controlify.compatibility.yacl;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.utils.NavigationHelper;
import dev.isxander.yacl.gui.controllers.slider.SliderControllerElement; import dev.isxander.yacl.gui.controllers.slider.SliderControllerElement;
public class SliderControllerElementComponentProcessor implements ComponentProcessor { public class SliderControllerElementComponentProcessor implements ComponentProcessor {
private final SliderControllerElement slider; private final SliderControllerElement slider;
private int ticksSinceIncrement = 0; private final NavigationHelper navigationHelper = new NavigationHelper(15, 3);
private boolean prevLeft, prevRight;
public SliderControllerElementComponentProcessor(SliderControllerElement element) { public SliderControllerElementComponentProcessor(SliderControllerElement element) {
this.slider = element; this.slider = element;
@ -16,24 +16,29 @@ public class SliderControllerElementComponentProcessor implements ComponentProce
@Override @Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) { public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
ticksSinceIncrement++;
var left = controller.bindings().CYCLE_OPT_BACKWARD.held(); 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 right = controller.bindings().CYCLE_OPT_FORWARD.held();
var rightPrev = controller.bindings().CYCLE_OPT_FORWARD.prevHeld();
if (left || right) { boolean repeatEventAvailable = navigationHelper.canNavigate();
if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || left != prevLeft || right != prevRight) {
slider.incrementValue(left ? -1 : 1); if (left && (repeatEventAvailable || !leftPrev)) {
ticksSinceIncrement = 0; slider.incrementValue(-1);
prevLeft = left;
prevRight = right; if (!leftPrev)
return true; navigationHelper.reset();
} } else if (right && (repeatEventAvailable || !rightPrev)) {
slider.incrementValue(1);
if (!rightPrev)
navigationHelper.reset();
} else { } else {
this.prevLeft = false; return false;
this.prevRight = false;
} }
return false; navigationHelper.onNavigate();
return true;
} }
} }

View File

@ -83,7 +83,7 @@ public class ControlifyConfig {
} }
controllerData = newControllerData; 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("controllers", controllerData);
config.add("compound_joysticks", GSON.toJsonTree(compoundJoysticks.values().toArray(new CompoundJoystickInfo[0]))); config.add("compound_joysticks", GSON.toJsonTree(compoundJoysticks.values().toArray(new CompoundJoystickInfo[0])));
config.add("global", GSON.toJsonTree(globalSettings)); config.add("global", GSON.toJsonTree(globalSettings));
@ -127,7 +127,7 @@ public class ControlifyConfig {
if (object.has("current_controller")) { if (object.has("current_controller")) {
currentControllerUid = object.get("current_controller").getAsString(); currentControllerUid = object.get("current_controller").getAsString();
} else { } else {
currentControllerUid = controlify.currentController().uid(); currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null);
setDirty(); setDirty();
} }
} }

View File

@ -1,12 +1,14 @@
package dev.isxander.controlify.config.gui; package dev.isxander.controlify.config.gui;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.api.bind.ControllerBinding; import dev.isxander.controlify.api.bind.ControllerBinding;
import dev.isxander.controlify.bindings.ControllerBindingImpl; import dev.isxander.controlify.bindings.ControllerBindingImpl;
import dev.isxander.controlify.bindings.IBind; import dev.isxander.controlify.bindings.IBind;
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.ControllerState; import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.controller.gamepad.GamepadState;
@ -42,6 +44,9 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
public class YACLHelper { public class YACLHelper {
private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
private static final Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v);
public static Screen generateConfigScreen(Screen parent) { public static Screen generateConfigScreen(Screen parent) {
var controlify = Controlify.instance(); var controlify = Controlify.instance();
@ -57,7 +62,7 @@ public class YACLHelper {
.option(Option.createBuilder((Class<Controller<?, ?>>) (Class<?>) Controller.class) .option(Option.createBuilder((Class<Controller<?, ?>>) (Class<?>) Controller.class)
.name(Component.translatable("controlify.gui.current_controller")) .name(Component.translatable("controlify.gui.current_controller"))
.tooltip(Component.translatable("controlify.gui.current_controller.tooltip")) .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()))) .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()) .build())
.option(globalVibrationOption = Option.createBuilder(boolean.class) .option(globalVibrationOption = Option.createBuilder(boolean.class)
@ -119,9 +124,6 @@ public class YACLHelper {
var config = controller.config(); var config = controller.config();
var def = controller.defaultConfig(); var def = controller.defaultConfig();
Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v);
var basicGroup = OptionGroup.createBuilder() var basicGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.basic")) .name(Component.translatable("controlify.gui.group.basic"))
.tooltip(Component.translatable("controlify.gui.group.basic.tooltip")) .tooltip(Component.translatable("controlify.gui.group.basic.tooltip"))
@ -208,145 +210,20 @@ public class YACLHelper {
.build()); .build());
category.group(basicGroup.build()); category.group(basicGroup.build());
var vibrationGroup = OptionGroup.createBuilder() if (controller.canRumble()) {
.name(Component.translatable("controlify.gui.group.vibration")) category.group(makeVibrationGroup(globalVibrationOption, config, def));
.tooltip(Component.translatable("controlify.gui.group.vibration.tooltip"));
List<Option<Float>> strengthOptions = new ArrayList<>();
Option<Boolean> 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);
} }
category.group(vibrationGroup.build());
if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) { if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) {
var gpCfg = gamepad.config(); category.group(makeGyroGroup(gamepad));
var gpCfgDef = gamepad.defaultConfig();
Option<Float> gyroSensitivity;
List<Option<?>> 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());
} }
var advancedGroup = OptionGroup.createBuilder() var advancedGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.advanced")) .name(Component.translatable("controlify.gui.group.advanced"))
.tooltip(Component.translatable("controlify.gui.group.advanced.tooltip")) .tooltip(Component.translatable("controlify.gui.group.advanced.tooltip"))
.collapsed(true) .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());
if (controller instanceof GamepadController gamepad) { addDeadzoneOptions(controller, advancedGroup);
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<Integer> 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());
}
}
advancedGroup advancedGroup
.option(ButtonOption.createBuilder() .option(ButtonOption.createBuilder()
@ -394,6 +271,139 @@ public class YACLHelper {
return category.build(); return category.build();
} }
private static OptionGroup makeVibrationGroup(Option<Boolean> 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<Option<Float>> strengthOptions = new ArrayList<>();
Option<Boolean> 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<Float> gyroSensitivity;
List<Option<?>> 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<Integer> 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 <T extends ControllerState> Map<Component, List<ControllerBinding>> groupBindings(Collection<ControllerBinding> bindings) { private static <T extends ControllerState> Map<Component, List<ControllerBinding>> groupBindings(Collection<ControllerBinding> bindings) {
return bindings.stream() return bindings.stream()
.collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList())); .collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList()));

View File

@ -44,7 +44,9 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
void clearState(); void clearState();
default void close() {} default void close() {}
RumbleManager rumbleManager(); RumbleManager rumbleManager();
boolean canRumble();
default boolean canBeUsed() { default boolean canBeUsed() {
return true; return true;
@ -88,6 +90,7 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
CONTROLLERS.remove(controller.uid(), controller); CONTROLLERS.remove(controller.uid(), controller);
} }
@Deprecated
Controller<?, ?> DUMMY = new Controller<>() { Controller<?, ?> DUMMY = new Controller<>() {
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this); private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
private final RumbleManager rumbleManager = new RumbleManager(new RumbleCapable() { private final RumbleManager rumbleManager = new RumbleManager(new RumbleCapable() {
@ -182,5 +185,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
public RumbleManager rumbleManager() { public RumbleManager rumbleManager() {
return rumbleManager; return rumbleManager;
} }
@Override
public boolean canRumble() {
return false;
}
}; };
} }

View File

@ -13,8 +13,6 @@ public abstract class ControllerConfig {
public float buttonActivationThreshold = 0.5f; public float buttonActivationThreshold = 0.5f;
public int screenRepeatNavigationDelay = 4;
public float virtualMouseSensitivity = 1f; public float virtualMouseSensitivity = 1f;
public boolean autoJump = false; public boolean autoJump = false;

View File

@ -1,16 +1,20 @@
package dev.isxander.controlify.controller.gamepad; package dev.isxander.controlify.controller.gamepad;
import dev.isxander.yacl.api.NameableEnum; import dev.isxander.yacl.api.NameableEnum;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
public enum BuiltinGamepadTheme implements NameableEnum { public enum BuiltinGamepadTheme implements NameableEnum {
DEFAULT("default"), DEFAULT("default", "default"),
XBOX_ONE("xbox_one"), XBOX_ONE("Xbox One", "xbox_one"),
DUALSHOCK4("dualshock4"); DUALSHOCK4("Dualshock 4", "dualshock4"),
DUALSHOCK3("Dualshock 3", "dualshock3"),
DUALSENSE("Dualsense", "dualsense");
private final String id; private final String name, id;
BuiltinGamepadTheme(String id) { BuiltinGamepadTheme(String name, String id) {
this.name = name;
this.id = id; this.id = id;
} }
@ -20,6 +24,9 @@ public enum BuiltinGamepadTheme implements NameableEnum {
@Override @Override
public Component getDisplayName() { public Component getDisplayName() {
return Component.translatable("controlify.controller_theme." + id().toLowerCase()); if (this == DEFAULT)
return Component.translatable("options.gamma.default");
return Component.literal(name);
} }
} }

View File

@ -6,6 +6,7 @@ import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.ScreenAccessor; import dev.isxander.controlify.mixins.feature.screenop.vanilla.ScreenAccessor;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.TabNavigationBarAccessor; import dev.isxander.controlify.mixins.feature.screenop.vanilla.TabNavigationBarAccessor;
import dev.isxander.controlify.utils.NavigationHelper;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.ComponentPath;
import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.components.events.GuiEventListener;
@ -22,7 +23,7 @@ import java.util.*;
public class ScreenProcessor<T extends Screen> { public class ScreenProcessor<T extends Screen> {
public final T screen; public final T screen;
protected int lastMoved = 0; protected final NavigationHelper navigationHelper = new NavigationHelper(10, 3);
protected final Minecraft minecraft = Minecraft.getInstance(); protected final Minecraft minecraft = Minecraft.getInstance();
public ScreenProcessor(T screen) { public ScreenProcessor(T screen) {
@ -66,26 +67,39 @@ public class ScreenProcessor<T extends Screen> {
var accessor = (ScreenAccessor) screen; var accessor = (ScreenAccessor) screen;
boolean repeatEventAvailable = ++lastMoved >= controller.config().screenRepeatNavigationDelay; boolean repeatEventAvailable = navigationHelper.canNavigate();
var bindings = controller.bindings(); var bindings = controller.bindings();
FocusNavigationEvent.ArrowNavigation event = null; FocusNavigationEvent.ArrowNavigation event = null;
if (bindings.GUI_NAVI_RIGHT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_RIGHT.prevHeld())) { if (bindings.GUI_NAVI_RIGHT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_RIGHT.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT); 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())) { } else if (bindings.GUI_NAVI_LEFT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_LEFT.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT); 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())) { } else if (bindings.GUI_NAVI_UP.held() && (repeatEventAvailable || !bindings.GUI_NAVI_UP.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.UP); 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())) { } else if (bindings.GUI_NAVI_DOWN.held() && (repeatEventAvailable || !bindings.GUI_NAVI_DOWN.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN); event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN);
if (!bindings.GUI_NAVI_DOWN.prevHeld())
navigationHelper.reset();
} }
if (event != null) { if (event != null) {
ComponentPath path = screen.nextFocusPath(event); ComponentPath path = screen.nextFocusPath(event);
if (path != null) { if (path != null) {
accessor.invokeChangeFocus(path); accessor.invokeChangeFocus(path);
lastMoved = 0;
navigationHelper.onNavigate();
var newFocusTree = getFocusTree(); var newFocusTree = getFocusTree();
while (!newFocusTree.isEmpty() && !focuses.contains(newFocusTree.peek())) { while (!newFocusTree.isEmpty() && !focuses.contains(newFocusTree.peek())) {
@ -156,7 +170,7 @@ public class ScreenProcessor<T extends Screen> {
ComponentPath path = screen.nextFocusPath(accessor.invokeCreateArrowEvent(ScreenDirection.DOWN)); ComponentPath path = screen.nextFocusPath(accessor.invokeCreateArrowEvent(ScreenDirection.DOWN));
if (path != null) { if (path != null) {
accessor.invokeChangeFocus(path); accessor.invokeChangeFocus(path);
lastMoved = 0; navigationHelper.clearDelay();
} }
} }
} }

View File

@ -3,36 +3,41 @@ package dev.isxander.controlify.screenop.compat;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.utils.NavigationHelper;
/** /**
* A component processor that handles incrementing and decrementing a slider. * 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. * 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 { public abstract class AbstractSliderComponentProcessor implements ComponentProcessor {
private int ticksSinceIncrement = 0; private final NavigationHelper navigationHelper = new NavigationHelper(15, 3);
private boolean prevLeft, prevRight;
@Override @Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) { public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) {
ticksSinceIncrement++;
var left = controller.bindings().CYCLE_OPT_BACKWARD.held(); 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 right = controller.bindings().CYCLE_OPT_FORWARD.held();
var rightPrev = controller.bindings().CYCLE_OPT_FORWARD.prevHeld();
if (left || right) { boolean repeatEventAvailable = navigationHelper.canNavigate();
if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || left != prevLeft || right != prevRight) {
incrementSlider(left); if (left && (repeatEventAvailable || !leftPrev)) {
ticksSinceIncrement = 0; incrementSlider(true);
prevLeft = left;
prevRight = right; if (!leftPrev)
return true; navigationHelper.reset();
} } else if (right && (repeatEventAvailable || !rightPrev)) {
incrementSlider(false);
if (!rightPrev)
navigationHelper.reset();
} else { } else {
this.prevLeft = false; return false;
this.prevRight = false;
} }
return false; navigationHelper.onNavigate();
return true;
} }
protected abstract void incrementSlider(boolean reverse); protected abstract void incrementSlider(boolean reverse);

View File

@ -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;
}
}
}