From beece493c3ae5e1473c818f5e9835423dde0921c Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 11 Apr 2023 11:04:47 +0100 Subject: [PATCH] gyro & look input modifier & deadzone bug --- .../api/event/ControlifyEvents.java | 20 ++++ .../api/ingameinput/LookInputModifier.java | 21 ++++ .../controlify/config/ControlifyConfig.java | 34 ++++--- .../controlify/config/gui/YACLHelper.java | 60 +++++++++++- .../controller/AbstractController.java | 44 +-------- .../controlify/controller/Controller.java | 49 ++++++---- .../controller/gamepad/GamepadConfig.java | 5 +- .../controller/gamepad/GamepadController.java | 95 ++++++++++++++++++- .../controller/gamepad/GamepadState.java | 37 +++++++- .../joystick/SingleJoystickController.java | 59 +++++++++++- .../ControllerDeadzoneCalibrationScreen.java | 9 +- .../controlify/ingame/InGameInputHandler.java | 79 ++++++++++++--- .../assets/controlify/lang/en_us.json | 8 ++ 13 files changed, 416 insertions(+), 104 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/api/ingameinput/LookInputModifier.java diff --git a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java index 6ee4e4e..137d8d9 100644 --- a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java +++ b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.api.event; import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.api.ingameinput.LookInputModifier; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry; @@ -44,6 +45,24 @@ public final class ControlifyEvents { } }); + public static final Event LOOK_INPUT_MODIFIER = EventFactory.createArrayBacked(LookInputModifier.class, callbacks -> new LookInputModifier() { + @Override + public float modifyX(float x, Controller controller) { + for (LookInputModifier callback : callbacks) { + x = callback.modifyX(x, controller); + } + return x; + } + + @Override + public float modifyY(float y, Controller controller) { + for (LookInputModifier callback : callbacks) { + y = callback.modifyY(y, controller); + } + return y; + } + }); + @FunctionalInterface public interface InputModeChanged { void onInputModeChanged(InputMode mode); @@ -63,4 +82,5 @@ public final class ControlifyEvents { public interface VirtualMouseToggled { void onVirtualMouseToggled(boolean enabled); } + } diff --git a/src/main/java/dev/isxander/controlify/api/ingameinput/LookInputModifier.java b/src/main/java/dev/isxander/controlify/api/ingameinput/LookInputModifier.java new file mode 100644 index 0000000..a7f813a --- /dev/null +++ b/src/main/java/dev/isxander/controlify/api/ingameinput/LookInputModifier.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.api.ingameinput; + +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.ingame.InGameInputHandler; + +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; + +public interface LookInputModifier { + float modifyX(float x, Controller controller); + + float modifyY(float y, Controller controller); + + static LookInputModifier functional(BiFunction, Float> x, BiFunction, Float> y) { + return new InGameInputHandler.FunctionalLookInputModifier(x, y); + } + + static LookInputModifier zeroIf(BooleanSupplier condition) { + return functional((x, controller) -> condition.getAsBoolean() ? 0 : x, (y, controller) -> condition.getAsBoolean() ? 0 : y); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index f15c335..4d7e881 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -11,7 +11,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -32,6 +31,7 @@ public class ControlifyConfig { private Map compoundJoysticks = Map.of(); private GlobalSettings globalSettings = new GlobalSettings(); private boolean firstLaunch; + private boolean dirty; public ControlifyConfig(Controlify controlify) { @@ -101,14 +101,19 @@ public class ControlifyConfig { private void applyConfig(JsonObject object) { globalSettings = GSON.fromJson(object.getAsJsonObject("global"), GlobalSettings.class); - if (globalSettings == null) globalSettings = new GlobalSettings(); + if (globalSettings == null) { + globalSettings = new GlobalSettings(); + setDirty(); + } JsonObject controllers = object.getAsJsonObject("controllers"); if (controllers != null) { this.controllerData = controllers; for (var controller : Controller.CONTROLLERS.values()) { - _loadOrCreateControllerData(controller); + loadOrCreateControllerData(controller); } + } else { + setDirty(); } this.compoundJoysticks = object @@ -122,25 +127,18 @@ public class ControlifyConfig { currentControllerUid = object.get("current_controller").getAsString(); } else { currentControllerUid = controlify.currentController().uid(); + setDirty(); } } - public boolean loadOrCreateControllerData(Controller controller) { - boolean result = _loadOrCreateControllerData(controller); - saveIfDirty(); - return result; - } - - private boolean _loadOrCreateControllerData(Controller controller) { + public void loadOrCreateControllerData(Controller controller) { var uid = controller.uid(); if (controllerData.has(uid)) { Controlify.LOGGER.info("Loading controller data for " + uid); applyControllerConfig(controller, controllerData.getAsJsonObject(uid)); - return true; } else { Controlify.LOGGER.info("New controller found, creating controller data for " + uid); - save(); - return false; + setDirty(); } } @@ -155,17 +153,17 @@ public class ControlifyConfig { } } - private void saveIfDirty() { + public void setDirty() { + dirty = true; + } + + public void saveIfDirty() { if (dirty) { Controlify.LOGGER.info("Config is dirty. Saving..."); save(); } } - public Optional getLoadedControllerConfig(String uid) { - return Optional.ofNullable(controllerData.getAsJsonObject(uid)); - } - public Map getCompoundJoysticks() { return compoundJoysticks; } 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 05dc584..c4f1d76 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -13,6 +13,7 @@ import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme; import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.controller.joystick.JoystickState; +import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.rumble.BasicRumbleEffect; @@ -236,6 +237,51 @@ public class YACLHelper { } category.group(vibrationGroup.build()); + if (controller instanceof GamepadController gamepad && (gamepad.hasGyro() || true)) { + 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()); + } + var advancedGroup = OptionGroup.createBuilder() .name(Component.translatable("controlify.gui.group.advanced")) .tooltip(Component.translatable("controlify.gui.group.advanced.tooltip")) @@ -274,11 +320,12 @@ public class YACLHelper { .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .build()); } else if (controller instanceof SingleJoystickController joystick) { - Collection deadzoneAxes = IntStream.range(0, joystick.axisCount()) - .filter(i -> joystick.mapping().axis(i).requiresDeadzone()) + JoystickMapping.Axis[] axes = joystick.mapping().axes(); + Collection deadzoneAxes = IntStream.range(0, axes.length) + .filter(i -> axes[i].requiresDeadzone()) .boxed() .collect(Collectors.toMap( - i -> joystick.mapping().axis(i).identifier(), + i -> axes[i].identifier(), i -> i, (x, y) -> x, LinkedHashMap::new @@ -288,9 +335,11 @@ public class YACLHelper { 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", joystick.mapping().axis(i).name())) - .tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", joystick.mapping().axis(i).name())) + .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)))) @@ -325,6 +374,7 @@ public class YACLHelper { .join(BasicRumbleEffect.constant(0f, 1f, 5)) .repeat(10) ) + .earlyFinish(BasicRumbleEffect.finishOnScreenChange()) ); }) .build()); diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index df86470..d42542e 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -24,8 +24,6 @@ public abstract class AbstractController bindings; protected C config, defaultConfig; @@ -39,9 +37,6 @@ public abstract class AbstractController(getClass()){}.getType()); } catch (Exception e) { Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead. Printing json: " + json.toString(), e); + Controlify.instance().config().setDirty(); return; } @@ -123,46 +119,10 @@ public abstract class AbstractController> CONTROLLERS = new HashMap<>(); - static Controller createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { - Optional uid = hidInfo.createControllerUID(); - if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { - return CONTROLLERS.get(uid.get()); - } + static Optional> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { + try { + Optional uid = hidInfo.createControllerUID(); + if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { + return Optional.of(CONTROLLERS.get(uid.get())); + } - if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) { - GamepadController controller = new GamepadController(joystickId, hidInfo); + if (hidInfo.type().dontLoad()) { + Controlify.LOGGER.warn("Preventing load of controller #" + joystickId + " because its type prevents loading."); + return Optional.empty(); + } + + if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) { + GamepadController controller = new GamepadController(joystickId, hidInfo); + CONTROLLERS.put(controller.uid(), controller); + return Optional.of(controller); + } + + SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo); CONTROLLERS.put(controller.uid(), controller); - return controller; + return Optional.of(controller); + } catch (Throwable e) { + CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickId); + CrashReportCategory category = crashReport.addCategory("Controller Info"); + category.setDetail("Joystick ID", joystickId); + category.setDetail("Controller identification", hidInfo.type()); + category.setDetail("HID path", hidInfo.path().orElse("N/A")); + throw new ReportedException(crashReport); } - - SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo); - CONTROLLERS.put(controller.uid(), controller); - return controller; } static void remove(Controller controller) { CONTROLLERS.remove(controller.uid(), controller); - controller.close(); } Controller DUMMY = new Controller<>() { diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java index e01301e..61f7fac 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java @@ -5,10 +5,13 @@ import dev.isxander.controlify.controller.ControllerConfig; public class GamepadConfig extends ControllerConfig { public float leftStickDeadzoneX = 0.2f; public float leftStickDeadzoneY = 0.2f; - public float rightStickDeadzoneX = 0.2f; public float rightStickDeadzoneY = 0.2f; + public float gyroLookSensitivity = 0f; + public boolean gyroRequiresButton = true; + public boolean flickStick = false; + public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT; @Override diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java index 90b9596..d3cbe70 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -1,7 +1,15 @@ package dev.isxander.controlify.controller.gamepad; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.hid.ControllerHIDService; +import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; +import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.rumble.RumbleManager; +import dev.isxander.controlify.rumble.RumbleSource; +import org.libsdl.SDL; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWGamepadState; @@ -9,6 +17,12 @@ public class GamepadController extends AbstractController unnamedAxes; private final List unnamedRawAxes; private final List unnamedButtons; - public GamepadState(AxesState gamepadAxes, AxesState rawGamepadAxes, ButtonState gamepadButtons) { + public GamepadState( + AxesState gamepadAxes, + AxesState rawGamepadAxes, + ButtonState gamepadButtons, + @Nullable GamepadState.GyroState gyroDelta, + GyroState absoluteGyroPos + ) { this.gamepadAxes = gamepadAxes; this.rawGamepadAxes = rawGamepadAxes; this.gamepadButtons = gamepadButtons; + this.gyroDelta = gyroDelta; + this.absoluteGyroPos = absoluteGyroPos; this.unnamedAxes = List.of( gamepadAxes.leftStickX(), @@ -90,6 +102,19 @@ public final class GamepadState implements ControllerState { return gamepadButtons; } + public GyroState gyroDelta() { + if (gyroDelta == null) return GyroState.ORIGIN; + return gyroDelta; + } + + public GyroState absoluteGyroPos() { + return absoluteGyroPos; + } + + public boolean supportsGyro() { + return gyroDelta != null; + } + @Override public boolean equals(Object obj) { if (obj == this) return true; @@ -213,4 +238,12 @@ public final class GamepadState implements ControllerState { return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick); } } + + public record GyroState(float pitch, float yaw, float roll) { + public static GyroState ORIGIN = new GyroState(0, 0, 0); + + public GyroState add(GyroState other) { + return new GyroState(pitch + other.pitch, yaw + other.yaw, roll + other.roll); + } + } } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java index 222bfe3..64fe071 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java @@ -2,12 +2,17 @@ package dev.isxander.controlify.controller.joystick; import com.google.gson.Gson; import com.google.gson.JsonElement; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping; import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; -import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping; -import org.lwjgl.glfw.GLFW; +import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; +import dev.isxander.controlify.rumble.RumbleManager; +import dev.isxander.controlify.rumble.RumbleSource; +import org.libsdl.SDL; import java.util.Objects; @@ -15,6 +20,10 @@ public class SingleJoystickController extends AbstractController controller; @@ -63,23 +66,59 @@ public class InGameInputHandler { protected void handlePlayerLookInput() { var player = this.minecraft.player; + var gamepad = controller instanceof GamepadController ? (GamepadController) controller : null; + // flick stick - turn 90 degrees immediately upon turning + // should be paired with gyro controls + if (gamepad != null && gamepad.config().flickStick) { + if (player != null) { + var turnAngle = 90 / 0.15f; // Entity#turn multiplies cursor delta by 0.15 to get rotation + + player.turn( + (controller.bindings().LOOK_RIGHT.justPressed() ? turnAngle : 0) + - (controller.bindings().LOOK_LEFT.justPressed() ? turnAngle : 0), + (controller.bindings().LOOK_DOWN.justPressed() ? turnAngle : 0) + - (controller.bindings().LOOK_UP.justPressed() ? turnAngle : 0) + ); + } + + return; + } + + // normal look input var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state(); var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state(); + impulseX *= Math.abs(impulseX); + impulseY *= Math.abs(impulseY); + + if (controller.config().reduceAimingSensitivity && player != null && player.isUsingItem()) { + float aimMultiplier = switch (player.getUseItem().getUseAnimation()) { + case BOW, CROSSBOW, SPEAR -> 0.6f; + case SPYGLASS -> 0.2f; + default -> 1f; + }; + impulseX *= aimMultiplier; + impulseY *= aimMultiplier; + } + + // gyro input + if (gamepad != null + && gamepad.hasGyro() + && (!gamepad.config().gyroRequiresButton || gamepad.bindings().GAMEPAD_GYRO_BUTTON.held()) + ) { + var gyroDelta = gamepad.state().gyroDelta(); + + impulseX += (gyroDelta.yaw() + gyroDelta.pitch()) * gamepad.config().gyroLookSensitivity; + impulseY += gyroDelta.roll() * gamepad.config().gyroLookSensitivity; + } + + LookInputModifier lookInputModifier = ControlifyEvents.LOOK_INPUT_MODIFIER.invoker(); + impulseX = lookInputModifier.modifyX(impulseX, controller); + impulseY = lookInputModifier.modifyY(impulseY, controller); if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive() && player != null) { - lookInputX = impulseX * Math.abs(impulseX) * controller.config().horizontalLookSensitivity; - lookInputY = impulseY * Math.abs(impulseY) * controller.config().verticalLookSensitivity; - - if (controller.config().reduceAimingSensitivity && player.isUsingItem()) { - float aimMultiplier = switch (player.getUseItem().getUseAnimation()) { - case BOW, CROSSBOW, SPEAR -> 0.6f; - case SPYGLASS -> 0.2f; - default -> 1f; - }; - lookInputX *= aimMultiplier; - lookInputY *= aimMultiplier; - } + lookInputX = impulseX * controller.config().horizontalLookSensitivity; + lookInputY = impulseY * controller.config().verticalLookSensitivity; } else { lookInputX = lookInputY = 0; } @@ -90,4 +129,16 @@ public class InGameInputHandler { minecraft.player.turn(lookInputX * 65f * deltaTime, lookInputY * 65f * deltaTime); } } + + public record FunctionalLookInputModifier(BiFunction, Float> x, BiFunction, Float> y) implements LookInputModifier { + @Override + public float modifyX(float x, Controller controller) { + return this.x.apply(x, controller); + } + + @Override + public float modifyY(float y, Controller controller) { + return this.y.apply(y, controller); + } + } } diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 8c13a67..ab9b588 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -49,6 +49,14 @@ "controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.", "controlify.gui.group.vibration": "Vibration", "controlify.gui.group.vibration.tooltip": "Adjust how your controller vibrates.", + "controlify.gui.group.gyro": "Gyro", + "controlify.gui.group.gyro.tooltip": "Adjust how Controlify treats your controller's built in gyroscope.\nA gyroscope determines how the controller is rotated.", + "controlify.gui.gyro_look_sensitivity": "Look Sensitivity", + "controlify.gui.gyro_look_sensitivity.tooltip": "How much the camera moves based on gyroscope rotation.", + "controlify.gui.gyro_requires_button": "Require Button", + "controlify.gui.gyro_requires_button.tooltip": "If the gyroscope should only be used when the gyro bind is pressed down. (scroll down to controls).", + "controlify.gui.flick_stick": "Flick Stick", + "controlify.gui.flick_stick.tooltip": "Changes the behaviour of the look up/down/left/right binds to rotate the look direction 90 degrees in the respected direction upon press. This should be combined with gyro look to get the most accurate and fast aiming.", "controlify.gui.group.advanced": "Advanced", "controlify.gui.group.advanced.tooltip": "Settings you probably shouldn't touch!.", "controlify.gui.screen_repeat_navi_delay": "Screen Repeat Navigation Delay",