forked from Clones/Controlify
controller axes calibration
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package dev.isxander.controlify;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
|
||||
import dev.isxander.controlify.config.ControlifyConfig;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
@ -14,10 +15,14 @@ import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
|
||||
public class Controlify {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static Controlify instance = null;
|
||||
@ -31,6 +36,8 @@ public class Controlify {
|
||||
|
||||
private final ControlifyConfig config = new ControlifyConfig();
|
||||
|
||||
private final Queue<Controller> calibrationQueue = new ArrayDeque<>();
|
||||
|
||||
public void onInitializeInput() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
|
||||
@ -44,7 +51,10 @@ public class Controlify {
|
||||
controllerHIDService.awaitNextController(device -> {
|
||||
setCurrentController(Controller.create(jid, device));
|
||||
LOGGER.info("Controller found: " + currentController.name());
|
||||
config().loadOrCreateControllerData(currentController);
|
||||
|
||||
if (!config().loadOrCreateControllerData(currentController)) {
|
||||
calibrationQueue.add(currentController);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -61,7 +71,9 @@ public class Controlify {
|
||||
LOGGER.info("Controller connected: " + currentController.name());
|
||||
this.setCurrentInputMode(InputMode.CONTROLLER);
|
||||
|
||||
config().loadOrCreateControllerData(currentController);
|
||||
if (!config().loadOrCreateControllerData(currentController)) {
|
||||
calibrationQueue.add(currentController);
|
||||
}
|
||||
|
||||
minecraft.getToasts().addToast(SystemToast.multiline(
|
||||
minecraft,
|
||||
@ -94,6 +106,24 @@ public class Controlify {
|
||||
}
|
||||
|
||||
public void tick(Minecraft client) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
if (minecraft.getOverlay() == null) {
|
||||
if (!calibrationQueue.isEmpty()) {
|
||||
Screen screen = minecraft.screen;
|
||||
while (!calibrationQueue.isEmpty()) {
|
||||
screen = new ControllerDeadzoneCalibrationScreen(calibrationQueue.poll(), screen);
|
||||
}
|
||||
minecraft.setScreen(screen);
|
||||
|
||||
minecraft.getToasts().addToast(SystemToast.multiline(
|
||||
minecraft,
|
||||
SystemToast.SystemToastIds.PERIODIC_NOTIFICATION,
|
||||
Component.translatable("controlify.toast.controller_calibration.title"),
|
||||
Component.translatable("controlify.toast.controller_calibration.description")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (Controller controller : Controller.CONTROLLERS.values()) {
|
||||
controller.updateState();
|
||||
}
|
||||
|
@ -91,12 +91,14 @@ public class ControlifyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public void loadOrCreateControllerData(Controller controller) {
|
||||
public boolean loadOrCreateControllerData(Controller controller) {
|
||||
var uid = controller.uid();
|
||||
if (controllerData.has(uid)) {
|
||||
applyControllerConfig(controller, controllerData.getAsJsonObject(uid));
|
||||
return true;
|
||||
} else {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import dev.isxander.controlify.bindings.IBind;
|
||||
import dev.isxander.controlify.config.GlobalSettings;
|
||||
import dev.isxander.controlify.controller.ControllerTheme;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||
import dev.isxander.yacl.api.*;
|
||||
import dev.isxander.yacl.gui.controllers.ActionController;
|
||||
import dev.isxander.yacl.gui.controllers.BooleanController;
|
||||
@ -153,6 +154,12 @@ public class YACLHelper {
|
||||
.binding(def.rightStickDeadzone, () -> config.rightStickDeadzone, v -> config.rightStickDeadzone = v)
|
||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
||||
.build())
|
||||
.option(ButtonOption.createBuilder()
|
||||
.name(Component.translatable("controlify.gui.auto_calibration"))
|
||||
.tooltip(Component.translatable("controlify.gui.auto_calibration.tooltip"))
|
||||
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen)))
|
||||
.controller(ActionController::new)
|
||||
.build())
|
||||
.option(Option.createBuilder(float.class)
|
||||
.name(Component.translatable("controlify.gui.button_activation_threshold"))
|
||||
.tooltip(Component.translatable("controlify.gui.button_activation_threshold.tooltip"))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package dev.isxander.controlify.controller;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.hid.HIDIdentifier;
|
||||
import org.hid4java.HidDevice;
|
||||
@ -57,17 +58,18 @@ public final class Controller {
|
||||
|
||||
prevState = state;
|
||||
|
||||
AxesState axesState = AxesState.fromController(this)
|
||||
AxesState rawAxesState = AxesState.fromController(this);
|
||||
AxesState axesState = rawAxesState
|
||||
.leftJoystickDeadZone(config().leftStickDeadzone, config().leftStickDeadzone)
|
||||
.rightJoystickDeadZone(config().rightStickDeadzone, config().rightStickDeadzone)
|
||||
.leftTriggerDeadZone(config().leftTriggerDeadzone)
|
||||
.rightTriggerDeadZone(config().rightTriggerDeadzone);
|
||||
ButtonState buttonState = ButtonState.fromController(this);
|
||||
state = new ControllerState(axesState, buttonState);
|
||||
state = new ControllerState(axesState, rawAxesState, buttonState);
|
||||
}
|
||||
|
||||
public void consumeButtonState() {
|
||||
this.state = new ControllerState(state().axes(), ButtonState.EMPTY);
|
||||
this.state = new ControllerState(state().axes(), state().rawAxes(), ButtonState.EMPTY);
|
||||
}
|
||||
|
||||
public ControllerBindings bindings() {
|
||||
@ -147,13 +149,15 @@ public final class Controller {
|
||||
String fallbackName = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
|
||||
String uid = device != null ? UUID.nameUUIDFromBytes(device.getPath().getBytes(StandardCharsets.UTF_8)).toString() : "unidentified-" + UUID.randomUUID();
|
||||
ControllerType type = device != null ? ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId())) : ControllerType.UNKNOWN;
|
||||
String name = type != ControllerType.UNKNOWN || fallbackName == null ? type.friendlyName() : fallbackName;
|
||||
String ogName = type != ControllerType.UNKNOWN || fallbackName == null ? type.friendlyName() : fallbackName;
|
||||
String name = ogName;
|
||||
int tries = 1;
|
||||
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equals)) {
|
||||
name = type.friendlyName() + " (" + tries++ + ")";
|
||||
name = ogName + " (" + tries++ + ")";
|
||||
}
|
||||
|
||||
Controller controller = new Controller(id, guid, name, gamepad, uid, type);
|
||||
|
||||
CONTROLLERS.put(id, controller);
|
||||
|
||||
return controller;
|
||||
|
@ -1,9 +1,9 @@
|
||||
package dev.isxander.controlify.controller;
|
||||
|
||||
public record ControllerState(AxesState axes, ButtonState buttons) {
|
||||
public static final ControllerState EMPTY = new ControllerState(AxesState.EMPTY, ButtonState.EMPTY);
|
||||
public record ControllerState(AxesState axes, AxesState rawAxes, ButtonState buttons) {
|
||||
public static final ControllerState EMPTY = new ControllerState(AxesState.EMPTY, AxesState.EMPTY, ButtonState.EMPTY);
|
||||
|
||||
public boolean hasAnyInput() {
|
||||
return !this.equals(EMPTY);
|
||||
return !this.axes().equals(AxesState.EMPTY) || !this.buttons().equals(ButtonState.EMPTY);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,160 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
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.math.RoundingMode;
|
||||
|
||||
public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
|
||||
|
||||
protected final Controller controller;
|
||||
private final Screen parent;
|
||||
|
||||
private MultiLineLabel waitLabel, infoLabel, completeLabel;
|
||||
|
||||
protected Button readyButton;
|
||||
|
||||
protected boolean calibrating = false, calibrated = false;
|
||||
protected int calibrationTicks = 0;
|
||||
|
||||
public ControllerDeadzoneCalibrationScreen(Controller controller, Screen parent) {
|
||||
super(Component.translatable("controlify.calibration.title"));
|
||||
this.controller = controller;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
addRenderableWidget(
|
||||
readyButton = Button.builder(Component.translatable("controlify.calibration.ready"), btn -> {
|
||||
if (!calibrated)
|
||||
startCalibration();
|
||||
else
|
||||
onClose();
|
||||
})
|
||||
.width(150)
|
||||
.pos(this.width / 2 - 75, this.height - 8 - 20)
|
||||
.build());
|
||||
|
||||
this.infoLabel = MultiLineLabel.create(font, Component.translatable("controlify.calibration.info"), width - 30);
|
||||
this.waitLabel = MultiLineLabel.create(font, Component.translatable("controlify.calibration.wait"), width - 30);
|
||||
this.completeLabel = MultiLineLabel.create(font, Component.translatable("controlify.calibration.complete"), width - 30);
|
||||
}
|
||||
|
||||
protected void startCalibration() {
|
||||
calibrating = true;
|
||||
|
||||
readyButton.active = false;
|
||||
readyButton.setMessage(Component.translatable("controlify.calibration.calibrating"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
|
||||
renderBackground(matrices);
|
||||
|
||||
super.render(matrices, mouseX, mouseY, delta);
|
||||
|
||||
drawCenteredString(matrices, font, Component.translatable("controlify.calibration.title", controller.name()).withStyle(ChatFormatting.BOLD), width / 2, 8, -1);
|
||||
|
||||
RenderSystem.setShaderTexture(0, GUI_BARS_LOCATION);
|
||||
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
|
||||
matrices.pushPose();
|
||||
matrices.scale(2f, 2f, 1f);
|
||||
drawBar(matrices, width / 2 / 2, 30 / 2, 1f, 0);
|
||||
var progress = (calibrationTicks - 1 + delta) / 100f;
|
||||
if (progress > 0)
|
||||
drawBar(matrices, width / 2 / 2, 30 / 2, progress, 5);
|
||||
matrices.popPose();
|
||||
|
||||
MultiLineLabel label;
|
||||
if (calibrating) label = waitLabel;
|
||||
else if (calibrated) label = completeLabel;
|
||||
else label = infoLabel;
|
||||
|
||||
label.renderCentered(matrices, width / 2, 55);
|
||||
}
|
||||
|
||||
private void drawBar(PoseStack matrices, int centerX, int y, float progress, int vOffset) {
|
||||
progress = 1 - (float)Math.pow(1 - progress, 3);
|
||||
|
||||
int x = centerX - 182 / 2;
|
||||
this.blit(matrices, x, y, 0, 30 + vOffset, (int)(progress * 182), 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (!calibrating)
|
||||
return;
|
||||
|
||||
if (stateChanged())
|
||||
calibrationTicks = 0;
|
||||
|
||||
if (calibrationTicks < 100) {
|
||||
calibrationTicks++;
|
||||
} else {
|
||||
useCurrentStateAsDeadzone();
|
||||
calibrating = false;
|
||||
calibrated = true;
|
||||
readyButton.active = true;
|
||||
readyButton.setMessage(Component.translatable("controlify.calibration.done"));
|
||||
}
|
||||
}
|
||||
|
||||
private void useCurrentStateAsDeadzone() {
|
||||
var rawAxes = controller.state().rawAxes();
|
||||
|
||||
var minDeadzoneLS = Math.max(rawAxes.leftStickX(), rawAxes.leftStickY()) + 0.08f;
|
||||
var deadzoneLS = (float)Mth.clamp(0.05 * Math.ceil(minDeadzoneLS / 0.05), 0, 0.95);
|
||||
|
||||
var minDeadzoneRS = Math.max(rawAxes.rightStickX(), rawAxes.rightStickY()) + 0.08f;
|
||||
var deadzoneRS = (float)Mth.clamp(0.05 * Math.ceil(minDeadzoneRS / 0.05), 0, 0.95);
|
||||
|
||||
controller.config().leftStickDeadzone = deadzoneLS;
|
||||
controller.config().rightStickDeadzone = deadzoneRS;
|
||||
}
|
||||
|
||||
private boolean stateChanged() {
|
||||
var amt = 0.0001f;
|
||||
|
||||
var lsX = controller.state().rawAxes().leftStickX();
|
||||
var prevLsX = controller.prevState().rawAxes().leftStickX();
|
||||
if (Math.abs(lsX - prevLsX) > amt)
|
||||
return true;
|
||||
|
||||
var lsY = controller.state().rawAxes().leftStickY();
|
||||
var prevLsY = controller.prevState().rawAxes().leftStickY();
|
||||
if (Math.abs(lsY - prevLsY) > amt)
|
||||
return true;
|
||||
|
||||
var rsX = controller.state().rawAxes().rightStickX();
|
||||
var prevRsX = controller.prevState().rawAxes().rightStickX();
|
||||
if (Math.abs(rsX - prevRsX) > amt)
|
||||
return true;
|
||||
|
||||
var rsY = controller.state().rawAxes().rightStickY();
|
||||
var prevRsY = controller.prevState().rawAxes().rightStickY();
|
||||
if (Math.abs(rsY - prevRsY) > amt)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
minecraft.setScreen(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldCloseOnEsc() {
|
||||
return false;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user