From 300817e561fc7df4dbf435c7607dd595070e8f3d Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 7 Mar 2023 20:03:29 +0000 Subject: [PATCH] manual controller switching & keyboard movement setting --- .../dev/isxander/controlify/Controlify.java | 122 ++++++++++-------- .../controlify/config/GlobalSettings.java | 1 + .../controlify/config/gui/YACLHelper.java | 6 + .../controller/ControllerConfig.java | 2 + .../controller/hid/ControllerHIDService.java | 11 +- .../ControllerDeadzoneCalibrationScreen.java | 4 + .../ingame/ControllerPlayerMovement.java | 6 + .../autoswitch/ToastComponentAccessor.java | 13 ++ .../isxander/controlify/utils/ToastUtils.java | 47 +++++++ .../assets/controlify/lang/en_us.json | 8 +- src/main/resources/controlify.accesswidener | 2 + src/main/resources/controlify.mixins.json | 1 + 12 files changed, 166 insertions(+), 57 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/autoswitch/ToastComponentAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/utils/ToastUtils.java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 053ff67..69f72a4 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -3,8 +3,6 @@ package dev.isxander.controlify; import com.mojang.blaze3d.Blaze3D; import com.mojang.logging.LogUtils; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.api.bind.ControlifyBindingsApi; -import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; @@ -15,6 +13,7 @@ import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.ingame.guide.InGameButtonGuide; import dev.isxander.controlify.ingame.InGameInputHandler; import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor; +import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.minecraft.client.Minecraft; @@ -32,6 +31,8 @@ public class Controlify implements ControlifyApi { public static final Logger LOGGER = LogUtils.getLogger(); private static Controlify instance = null; + private final Minecraft minecraft = Minecraft.getInstance(); + private Controller currentController = Controller.DUMMY; private InGameInputHandler inGameInputHandler; public InGameButtonGuide inGameButtonGuide; @@ -46,11 +47,13 @@ public class Controlify implements ControlifyApi { private int consecutiveInputSwitches = 0; private double lastInputSwitchTime = 0; + private Controller switchableController = null; + private double askSwitchTime = 0; + private ToastUtils.ControlifyToast askSwitchToast = null; + public void initializeControllers() { LOGGER.info("Discovering and initializing controllers..."); - Minecraft minecraft = Minecraft.getInstance(); - config().load(); controllerHIDService = new ControllerHIDService(); @@ -65,9 +68,7 @@ public class Controlify implements ControlifyApi { if (config().currentControllerUid().equals(controller.uid())) setCurrentController(controller); - if (!config().loadOrCreateControllerData(controller)) { - calibrationQueue.add(controller); - } + config().loadOrCreateControllerData(controller); } } @@ -78,39 +79,9 @@ public class Controlify implements ControlifyApi { // listen for new controllers GLFW.glfwSetJoystickCallback((jid, event) -> { if (event == GLFW.GLFW_CONNECTED) { - var firstController = Controller.CONTROLLERS.values().isEmpty(); - var controller = Controller.createOrGet(jid, controllerHIDService.fetchType()); - LOGGER.info("Controller connected: " + controller.name()); - - if (firstController) { - this.setCurrentController(controller); - this.setInputMode(InputMode.CONTROLLER); - } - - if (!config().loadOrCreateControllerData(currentController)) { - calibrationQueue.add(currentController); - } - - minecraft.getToasts().addToast(SystemToast.multiline( - minecraft, - SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, - Component.translatable("controlify.toast.controller_connected.title"), - Component.translatable("controlify.toast.controller_connected.description", currentController.name()) - )); + this.onControllerHotplugged(jid); } else if (event == GLFW.GLFW_DISCONNECTED) { - var controller = Controller.CONTROLLERS.remove(jid); - if (controller != null) { - setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); - LOGGER.info("Controller disconnected: " + controller.name()); - this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); - - minecraft.getToasts().addToast(SystemToast.multiline( - minecraft, - SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, - Component.translatable("controlify.toast.controller_disconnected.title"), - Component.translatable("controlify.toast.controller_disconnected.description", controller.name()) - )); - } + this.onControllerDisconnect(jid); } }); @@ -131,13 +102,6 @@ public class Controlify implements ControlifyApi { 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") - )); } } @@ -146,20 +110,32 @@ public class Controlify implements ControlifyApi { } ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state(); + + if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) { + if (switchableController.state().hasAnyInput()) { + this.setCurrentController(switchableController); + if (askSwitchToast != null) { + askSwitchToast.remove(); + askSwitchToast = null; + } + switchableController = null; + state = ControllerState.EMPTY; + } + } + if (!config().globalSettings().outOfFocusInput && !client.isWindowActive()) state = ControllerState.EMPTY; if (state.hasAnyInput()) this.setInputMode(InputMode.CONTROLLER); - if (consecutiveInputSwitches > 20) { + if (consecutiveInputSwitches > 500) { LOGGER.warn("Controlify detected current controller to be constantly giving input and has been disabled."); - minecraft.getToasts().addToast(SystemToast.multiline( - minecraft, - SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, + ToastUtils.sendToast( Component.translatable("controlify.toast.faulty_input.title"), - Component.translatable("controlify.toast.faulty_input.description") - )); + Component.translatable("controlify.toast.faulty_input.description"), + true + ); this.setCurrentController(null); consecutiveInputSwitches = 0; } @@ -184,6 +160,41 @@ public class Controlify implements ControlifyApi { return config; } + private void onControllerHotplugged(int jid) { + var controller = Controller.createOrGet(jid, controllerHIDService.fetchType()); + LOGGER.info("Controller connected: " + controller.name()); + + config().loadOrCreateControllerData(currentController); + + this.askToSwitchController(controller); + } + + private void onControllerDisconnect(int jid) { + var controller = Controller.CONTROLLERS.remove(jid); + if (controller != null) { + setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); + LOGGER.info("Controller disconnected: " + controller.name()); + this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); + + ToastUtils.sendToast( + Component.translatable("controlify.toast.controller_disconnected.title"), + Component.translatable("controlify.toast.controller_disconnected.description", controller.name()), + false + ); + } + } + + private void askToSwitchController(Controller controller) { + this.switchableController = controller; + this.askSwitchTime = Blaze3D.getTime(); + + askSwitchToast = ToastUtils.sendToast( + Component.translatable("controlify.toast.ask_to_switch.title"), + Component.translatable("controlify.toast.ask_to_switch.description", controller.name()), + true + ); + } + @Override public @NotNull Controller currentController() { if (currentController == null) @@ -199,6 +210,10 @@ public class Controlify implements ControlifyApi { if (this.currentController == controller) return; this.currentController = controller; + if (switchableController == controller) { + switchableController = null; + } + LOGGER.info("Updated current controller to " + controller.name() + "(" + controller.uid() + ")"); if (!config().currentControllerUid().equals(controller.uid())) { @@ -209,6 +224,9 @@ public class Controlify implements ControlifyApi { if (Minecraft.getInstance().player != null) { this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player); } + + if (!controller.config().calibrated && controller != Controller.DUMMY) + calibrationQueue.add(controller); } public InGameInputHandler inGameInputHandler() { diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index 693fbfd..ca9bc6e 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -12,5 +12,6 @@ public class GlobalSettings { AbstractContainerScreen.class ); + public boolean keyboardMovement = false; public boolean outOfFocusInput = false; } 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 7cd584f..ad63cc5 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -56,6 +56,12 @@ public class YACLHelper { .binding(GlobalSettings.DEFAULT.outOfFocusInput, () -> globalSettings.outOfFocusInput, v -> globalSettings.outOfFocusInput = v) .controller(TickBoxController::new) .build()) + .option(Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.keyboard_movement")) + .tooltip(Component.translatable("controlify.gui.keyboard_movement.tooltip")) + .binding(GlobalSettings.DEFAULT.keyboardMovement, () -> globalSettings.keyboardMovement, v -> globalSettings.keyboardMovement = v) + .controller(TickBoxController::new) + .build()) .option(ButtonOption.createBuilder() .name(Component.translatable("controlify.gui.open_issue_tracker")) .action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues")) diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java index 3b09256..ddc5593 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java @@ -18,6 +18,8 @@ public abstract class ControllerConfig { public boolean showGuide = true; + public boolean calibrated = 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/hid/ControllerHIDService.java b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java index 54f8411..f177adc 100644 --- a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java +++ b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java @@ -9,6 +9,7 @@ import java.util.*; public class ControllerHIDService implements HidServicesListener { private final HidServicesSpecification specification; + private HidServices services; private final Map unconsumedHIDs; private boolean disabled = false; @@ -21,7 +22,7 @@ public class ControllerHIDService implements HidServicesListener { public void start() { try { - var services = HidManager.getHidServices(specification); + services = HidManager.getHidServices(specification); services.addHidServicesListener(this); services.start(); @@ -32,6 +33,14 @@ public class ControllerHIDService implements HidServicesListener { } public ControllerHIDInfo fetchType() { + services.scan(); + try { + // wait for scan to complete on separate thread + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + var typeMap = ControllerType.getTypeMap(); for (var entry : unconsumedHIDs.entrySet()) { var path = entry.getKey(); 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 66ddad2..ff27b56 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.gui.screen; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.Controller; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.components.Button; @@ -104,6 +105,9 @@ public class ControllerDeadzoneCalibrationScreen extends Screen { calibrated = true; readyButton.active = true; readyButton.setMessage(Component.translatable("controlify.calibration.done")); + + controller.config().calibrated = true; + Controlify.instance().config().save(); } } diff --git a/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java index 9469ba7..2682320 100644 --- a/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java +++ b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java @@ -1,5 +1,6 @@ package dev.isxander.controlify.ingame; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.Controller; import net.minecraft.client.Minecraft; import net.minecraft.client.player.Input; @@ -39,6 +40,11 @@ public class ControllerPlayerMovement extends Input { this.left = bindings.WALK_LEFT.state() > 0.1; this.right = bindings.WALK_RIGHT.state() > 0.1; + if (Controlify.instance().config().globalSettings().keyboardMovement) { + this.forwardImpulse = Math.signum(this.forwardImpulse); + this.leftImpulse = Math.signum(this.leftImpulse); + } + if (slowDown) { this.leftImpulse *= f; this.forwardImpulse *= f; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/autoswitch/ToastComponentAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/autoswitch/ToastComponentAccessor.java new file mode 100644 index 0000000..fcfaccd --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/autoswitch/ToastComponentAccessor.java @@ -0,0 +1,13 @@ +package dev.isxander.controlify.mixins.feature.autoswitch; + +import net.minecraft.client.gui.components.toasts.ToastComponent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(ToastComponent.class) +public interface ToastComponentAccessor { + @Accessor + List> getVisible(); +} diff --git a/src/main/java/dev/isxander/controlify/utils/ToastUtils.java b/src/main/java/dev/isxander/controlify/utils/ToastUtils.java new file mode 100644 index 0000000..46a76fa --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/ToastUtils.java @@ -0,0 +1,47 @@ +package dev.isxander.controlify.utils; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.components.toasts.ToastComponent; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ToastUtils { + public static ControlifyToast sendToast(Component title, Component message, boolean longer) { + ControlifyToast toast = ControlifyToast.create(title, message, longer); + Minecraft.getInstance().getToasts().addToast(toast); + return toast; + } + + public static class ControlifyToast extends SystemToast { + private boolean removed; + + private ControlifyToast(Component title, List description, int maxWidth, boolean longer) { + super(longer ? SystemToastIds.UNSECURE_SERVER_WARNING : SystemToastIds.PERIODIC_NOTIFICATION, title, description, maxWidth); + } + + @Override + public @NotNull Visibility render(@NotNull PoseStack matrices, @NotNull ToastComponent manager, long startTime) { + if (removed) + return Visibility.HIDE; + + return super.render(matrices, manager, startTime); + } + + public void remove() { + this.removed = true; + } + + public static ControlifyToast create(Component title, Component message, boolean longer) { + Font font = Minecraft.getInstance().font; + List list = font.split(message, 200); + int i = Math.max(200, list.stream().mapToInt(font::width).max().orElse(200)); + return new ControlifyToast(title, list, i + 30, longer); + } + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index c5694a7..1ea48b6 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -4,6 +4,8 @@ "controlify.gui.current_controller.tooltip": "In Controlify's infancy, only one controller can be used at a time, this selects which one you want to use.", "controlify.gui.out_of_focus_input": "Out of Focus Input", "controlify.gui.out_of_focus_input.tooltip": "If enabled, Controlify will still receive input even if the game window is not focused.", + "controlify.gui.keyboard_movement": "Keyboard-like Movement", + "controlify.gui.keyboard_movement.tooltip": "Makes movement either on or off rather than being smooth with a thumbstick, this may help in cases where server anti-cheats are harsh.", "controlify.gui.open_issue_tracker": "Open Issue Tracker", "controlify.gui.group.basic": "Basic", @@ -55,12 +57,10 @@ "controlify.toast.vmouse_enabled.description": "Controlify virtual mouse is now enabled for this screen.", "controlify.toast.vmouse_disabled.title": "Virtual Mouse Disabled", "controlify.toast.vmouse_disabled.description": "Controlify virtual mouse is now disabled for this screen.", - "controlify.toast.controller_connected.title": "Controller Connected", - "controlify.toast.controller_connected.description": "A controller named '%s' has just been connected. You can switch to your other controller in Controlify settings.", + "controlify.toast.ask_to_switch.title": "Switch Controller?", + "controlify.toast.ask_to_switch.description": "A new controller named '%s' has just been connected. Press any button to switch to it.", "controlify.toast.controller_disconnected.title": "Controller Disconnected", "controlify.toast.controller_disconnected.description": "'%s' was disconnected.", - "controlify.toast.controller_calibration.title": "New controller detected", - "controlify.toast.controller_calibration.description": "A new controller(s) has been detected, you must calibrate before you use it!", "controlify.toast.faulty_input.title": "Controller disabled", "controlify.toast.faulty_input.description": "Your controller has been disabled because Controlify detected it was causing you problems using keyboard and mouse input. This is likely due to setting your deadzone values too low or your joystick is not mapped properly, making the controller think it is always giving input.", diff --git a/src/main/resources/controlify.accesswidener b/src/main/resources/controlify.accesswidener index d60b1d8..ac7ae6f 100644 --- a/src/main/resources/controlify.accesswidener +++ b/src/main/resources/controlify.accesswidener @@ -1,3 +1,5 @@ accessWidener v2 named accessible class net/minecraft/client/gui/screens/LanguageSelectScreen$LanguageSelectionList +accessible class net/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance +accessible method net/minecraft/client/gui/components/toasts/SystemToast (Lnet/minecraft/client/gui/components/toasts/SystemToast$SystemToastIds;Lnet/minecraft/network/chat/Component;Ljava/util/List;I)V diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 002cd8d..d3b4a5f 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -21,6 +21,7 @@ "core.MinecraftMixin", "core.MouseHandlerMixin", "feature.accessibility.LocalPlayerMixin", + "feature.autoswitch.ToastComponentAccessor", "feature.bind.KeyMappingAccessor", "feature.guide.ClientPacketListenerMixin", "feature.guide.GuiMixin",