diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 0e779f3..5417e99 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -453,7 +453,7 @@ public class Controlify implements ControlifyApi { this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player); } - if (!controller.config().calibrated) + if (!controller.config().deadzonesCalibrated) calibrationQueue.add(controller); } diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java index 6ce2aa3..505e3c6 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java @@ -3,9 +3,6 @@ package dev.isxander.controlify.controller; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import dev.isxander.controlify.rumble.RumbleSource; -import net.minecraft.resources.ResourceLocation; - -import java.util.Map; public abstract class ControllerConfig { public float horizontalLookSensitivity = 1f; @@ -31,7 +28,7 @@ public abstract class ControllerConfig { public boolean allowVibrations = true; public JsonObject vibrationStrengths = RumbleSource.getDefaultJson(); - public boolean calibrated = false; + public boolean deadzonesCalibrated = false; public abstract void setDeadzone(int axis, float deadzone); public abstract float getDeadzone(int axis); 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 ff64506..9ee2dfe 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java @@ -3,10 +3,13 @@ package dev.isxander.controlify.controller.gamepad; 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; + private float leftStickDeadzone = 0.15f; + private float rightStickDeadzone = 0.15f; + + private transient float leftStickDeadzoneX = leftStickDeadzone; + private transient float leftStickDeadzoneY = leftStickDeadzone; + private transient float rightStickDeadzoneX = rightStickDeadzone; + private transient float rightStickDeadzoneY = rightStickDeadzone; public float gyroLookSensitivity = 0f; public boolean gyroRequiresButton = true; @@ -16,6 +19,26 @@ public class GamepadConfig extends ControllerConfig { public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT; + public float getLeftStickDeadzone() { + return leftStickDeadzone; + } + + public float getRightStickDeadzone() { + return rightStickDeadzone; + } + + public void setLeftStickDeadzone(float deadzone) { + leftStickDeadzoneX = deadzone; + leftStickDeadzoneY = deadzone; + leftStickDeadzone = deadzone; + } + + public void setRightStickDeadzone(float deadzone) { + rightStickDeadzoneX = deadzone; + rightStickDeadzoneY = deadzone; + rightStickDeadzone = deadzone; + } + @Override public void setDeadzone(int axis, float deadzone) { switch (axis) { @@ -23,17 +46,20 @@ public class GamepadConfig extends ControllerConfig { case 1 -> leftStickDeadzoneY = deadzone; case 2 -> rightStickDeadzoneX = deadzone; case 3 -> rightStickDeadzoneY = deadzone; + case 4, 5 -> {} // ignore triggers default -> {} } + + leftStickDeadzone = Math.max(leftStickDeadzoneX, leftStickDeadzoneY); + rightStickDeadzone = Math.max(rightStickDeadzoneX, rightStickDeadzoneY); } @Override public float getDeadzone(int axis) { return switch (axis) { - case 0 -> leftStickDeadzoneX; - case 1 -> leftStickDeadzoneY; - case 2 -> rightStickDeadzoneX; - case 3 -> rightStickDeadzoneY; + case 0, 1 -> leftStickDeadzone; + case 2, 3 -> rightStickDeadzone; + case 4, 5 -> 0f; // ignore triggers default -> throw new IllegalArgumentException("Unknown axis: " + axis); }; } 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 884f59b..48896ba 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -65,8 +65,8 @@ public class GamepadController extends AbstractController 0) { diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadState.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadState.java index e35fcbf..f6e3d1f 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadState.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadState.java @@ -145,19 +145,19 @@ public final class GamepadState implements ControllerState { ) { public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0); - public AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) { + public AxesState leftJoystickDeadZone(float deadZone) { return new AxesState( - ControllerUtils.deadzone(leftStickX, deadZoneX), - ControllerUtils.deadzone(leftStickY, deadZoneY), + ControllerUtils.deadzone(leftStickX, deadZone), + ControllerUtils.deadzone(leftStickY, deadZone), rightStickX, rightStickY, leftTrigger, rightTrigger ); } - public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) { + public AxesState rightJoystickDeadZone(float deadZone) { return new AxesState( leftStickX, leftStickY, - ControllerUtils.deadzone(rightStickX, deadZoneX), - ControllerUtils.deadzone(rightStickY, deadZoneY), + ControllerUtils.deadzone(rightStickX, deadZone), + ControllerUtils.deadzone(rightStickY, deadZone), leftTrigger, rightTrigger ); } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 20b7a95..f24e447 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -177,42 +177,51 @@ public class ControllerConfigScreenFactory { } private static OptionGroup makeDeadzoneGroup(Controller controller, ControllerConfig def, ControllerConfig config) { + var deadzoneOpts = new ArrayList>(); + var group = OptionGroup.createBuilder() .name(Component.translatable("controlify.config.group.deadzones")); if (controller instanceof GamepadController gamepad) { var gpCfg = gamepad.config(); var gpCfgDef = gamepad.defaultConfig(); - group - .option(Option.createBuilder() - .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick"))) - .description(OptionDescription.createBuilder() - .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick"))) - .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) - .build()) - .binding( - Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY), - () -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY), - v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v - ) - .controller(opt -> FloatSliderControllerBuilder.create(opt) - .range(0f, 1f).step(0.01f) - .valueFormatter(percentFormatter)) + + Option left = Option.createBuilder() + .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick"))) + .description(OptionDescription.createBuilder() + .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick"))) + .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .build()) - .option(Option.createBuilder() - .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick"))) - .description(OptionDescription.createBuilder() - .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick"))) - .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) - .build()) - .binding( - Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY), - () -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY), - v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v - ) - .controller(opt -> FloatSliderControllerBuilder.create(opt) - .range(0f, 1f).step(0.01f) - .valueFormatter(percentFormatter)) - .build()); + .binding( + gpCfgDef.getLeftStickDeadzone(), + gpCfg::getLeftStickDeadzone, + gpCfg::setLeftStickDeadzone + ) + .controller(opt -> FloatSliderControllerBuilder.create(opt) + .range(0f, 1f).step(0.01f) + .valueFormatter(percentFormatter)) + .build(); + + Option right = Option.createBuilder() + .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick"))) + .description(OptionDescription.createBuilder() + .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick"))) + .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) + .build()) + .binding( + gpCfgDef.getRightStickDeadzone(), + gpCfg::getRightStickDeadzone, + gpCfg::setRightStickDeadzone + ) + .controller(opt -> FloatSliderControllerBuilder.create(opt) + .range(0f, 1f).step(0.01f) + .valueFormatter(percentFormatter)) + .build(); + + group.option(left); + group.option(right); + + deadzoneOpts.add(left); + deadzoneOpts.add(right); } else if (controller instanceof SingleJoystickController joystick) { JoystickMapping.Axis[] axes = joystick.mapping().axes(); Collection deadzoneAxes = IntStream.range(0, axes.length) @@ -231,7 +240,7 @@ public class ControllerConfigScreenFactory { for (int i : deadzoneAxes) { var axis = axes[i]; - group.option(Option.createBuilder() + Option deadzoneOpt = Option.createBuilder() .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name())) .description(OptionDescription.createBuilder() .text(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name())) @@ -241,7 +250,9 @@ public class ControllerConfigScreenFactory { .controller(opt -> FloatSliderControllerBuilder.create(opt) .range(0f, 1f).step(0.01f) .valueFormatter(percentFormatter)) - .build()); + .build(); + group.option(deadzoneOpt); + deadzoneOpts.add(deadzoneOpt); } } @@ -261,7 +272,10 @@ public class ControllerConfigScreenFactory { .description(OptionDescription.createBuilder() .text(Component.translatable("controlify.gui.auto_calibration.tooltip")) .build()) - .action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen))) + .action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, () -> { + deadzoneOpts.forEach(Option::forgetPendingValue); + return screen; + }))) .build()); return group.build(); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java index cdd222c..a1f6a6b 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java @@ -1,8 +1,8 @@ package dev.isxander.controlify.gui.screen; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; -import dev.isxander.controlify.utils.Log; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -10,13 +10,18 @@ import net.minecraft.client.gui.components.MultiLineLabel; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; public class ControllerDeadzoneCalibrationScreen extends Screen { + private static final int CALIBRATION_TIME = 100; private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png"); protected final Controller controller; - private final Screen parent; + private final Supplier parent; private MultiLineLabel waitLabel, infoLabel, completeLabel; @@ -25,7 +30,13 @@ public class ControllerDeadzoneCalibrationScreen extends Screen { protected boolean calibrating = false, calibrated = false; protected int calibrationTicks = 0; + private final Map calibrationData = new HashMap<>(); + public ControllerDeadzoneCalibrationScreen(Controller controller, Screen parent) { + this(controller, () -> parent); + } + + public ControllerDeadzoneCalibrationScreen(Controller controller, Supplier parent) { super(Component.translatable("controlify.calibration.title")); this.controller = controller; this.parent = parent; @@ -104,42 +115,45 @@ public class ControllerDeadzoneCalibrationScreen extends Screen { if (!calibrating) return; - if (stateChanged()) + if (stateChanged()) { calibrationTicks = 0; + calibrationData.clear(); + } + + if (calibrationTicks < CALIBRATION_TIME) { + calibrate(calibrationTicks); - if (calibrationTicks < 100) { calibrationTicks++; } else { - useCurrentStateAsDeadzone(); + applyDeadzones(); calibrating = false; calibrated = true; readyButton.active = true; readyButton.setMessage(Component.translatable("controlify.calibration.done")); - controller.config().calibrated = true; + controller.config().deadzonesCalibrated = true; Controlify.instance().config().save(); } } - private void useCurrentStateAsDeadzone() { - var axes = controller.state().axes(); + private void calibrate(int tick) { + var axes = controller.state().rawAxes(); for (int i = 0; i < axes.size(); i++) { - var axis = axes.get(i); - var minDeadzone = axis + 0.08f; - var deadzone = (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95); - - if (Float.isNaN(deadzone)) { - Log.LOGGER.warn("Deadzone for axis {} is NaN, using default deadzone.", i); - deadzone = controller.defaultConfig().getDeadzone(i); - } - - controller.config().setDeadzone(i, deadzone); + var axis = Math.abs(axes.get(i)); + calibrationData.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis; } } + private void applyDeadzones() { + calibrationData.forEach((i, data) -> { + var max = Arrays.stream(data).max().orElseThrow(); + controller.config().setDeadzone(i, (float) max + 0.05f); + }); + } + private boolean stateChanged() { - var amt = 0.0001f; + var amt = 0.4f; return controller.state().axes().stream() .anyMatch(axis -> Math.abs(axis - controller.prevState().axes().get(controller.state().axes().indexOf(axis))) > amt); @@ -147,7 +161,7 @@ public class ControllerDeadzoneCalibrationScreen extends Screen { @Override public void onClose() { - minecraft.setScreen(parent); + minecraft.setScreen(parent.get()); } @Override diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 5ccefdd..7096d41 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -251,8 +251,8 @@ "controlify.guide.container.quick_move": "Quick Move", "controlify.calibration.title": "Controller Calibration for '%s'", - "controlify.calibration.info": "This process will optimize settings for your controller to prevent stick drift. Stick drift happens in your controller thumbsticks and outputs slightly wrong values when you aren't touching them at all. Deadzones are used to prevent this.\n\nThis will only take a few seconds.", - "controlify.calibration.wait": "Please do not touch your controller thumbsticks until the progress bar is complete. This process will only take a few seconds.", + "controlify.calibration.info": "This process will optimize settings for your controller to prevent stick drift. Stick drift happens in your controller thumbsticks and outputs slightly wrong values when you aren't touching them at all. Deadzones are used to prevent this.\nShaking your controller lightly (without touching thumbsticks) can also aid in getting a more precise calibration.\n\nThis will only take a few seconds.", + "controlify.calibration.wait": "Please do not touch your controller thumbsticks until the progress bar is complete. Shaking your controller lightly can improve calibration.\nThis process will only take a few seconds.", "controlify.calibration.complete": "Calibration complete! You can now use your controller. Press done to return to the game.", "controlify.calibration.ready": "Ready", "controlify.calibration.done": "Done", diff --git a/src/testmod/java/dev/isxander/controlify/test/FakeController.java b/src/testmod/java/dev/isxander/controlify/test/FakeController.java index 5af4102..38119d1 100644 --- a/src/testmod/java/dev/isxander/controlify/test/FakeController.java +++ b/src/testmod/java/dev/isxander/controlify/test/FakeController.java @@ -50,7 +50,7 @@ public class FakeController implements JoystickController { return false; } }); - this.config.calibrated = true; + this.config.deadzonesCalibrated = true; } @Override