/* * Copyright © 2019 LambdAurora * * This file is part of LambdaControls. * * Licensed under the MIT license. For more information, * see the LICENSE file. */ package me.lambdaurora.lambdacontrols; import me.lambdaurora.lambdacontrols.mixin.AbstractContainerScreenAccessor; import me.lambdaurora.lambdacontrols.util.CreativeInventoryScreenAccessor; import me.lambdaurora.lambdacontrols.util.LambdaKeyBinding; import me.lambdaurora.lambdacontrols.util.MouseAccessor; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.Element; import net.minecraft.client.gui.ParentElement; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.ingame.AbstractContainerScreen; import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; import net.minecraft.client.gui.screen.world.WorldListWidget; import net.minecraft.client.gui.widget.*; import net.minecraft.client.options.KeyBinding; import net.minecraft.container.Slot; import net.minecraft.item.ItemGroup; import net.minecraft.util.math.MathHelper; import org.aperlambda.lambdacommon.utils.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Optional; public class ControllerInput { private static final Map BUTTON_STATES = new HashMap<>(); private static final Map BUTTON_COOLDOWNS = new HashMap<>(); private static final Map AXE_STATES = new HashMap<>(); private final LambdaControls mod; private final LambdaControlsConfig config; private int controller = GLFW.GLFW_JOYSTICK_3; private int action_gui_cooldown = 0; private boolean last_a_state = false; private boolean continuous_sneak = false; private int last_sneak = 0; private double prev_target_yaw = 0.0; private double prev_target_pitch = 0.0; private double target_yaw = 0.0; private double target_pitch = 0.0; private float prev_x_axis = 0.F; private float prev_y_axis = 0.F; private int prev_target_mouse_x = 0; private int prev_target_mouse_y = 0; private int target_mouse_x = 0; private int target_mouse_y = 0; private float mouse_speed_x = 0.F; private float mouse_speed_y = 0.F; public ControllerInput(@NotNull LambdaControls mod) { this.mod = mod; this.config = mod.config; } /** * This method is called every Minecraft tick. * * @param client The client instance. */ public void on_tick(@NotNull MinecraftClient client) { BUTTON_COOLDOWNS.entrySet().stream().filter(entry -> entry.getValue() > 0).forEach(entry -> BUTTON_COOLDOWNS.put(entry.getKey(), entry.getValue() - 1)); // Decreases the last_sneak counter which allows to double press to sneak continuously. if (this.last_sneak > 0) --this.last_sneak; // Decreases the cooldown for GUI actions. if (this.action_gui_cooldown > 0) --this.action_gui_cooldown; this.prev_target_yaw = this.target_yaw; this.prev_target_pitch = this.target_pitch; this.fetch_button_input(client); this.fetch_axe_input(client); } public void on_pre_render_screen(@NotNull MinecraftClient client, @NotNull Screen screen) { if (this.prev_target_mouse_x != this.target_mouse_x || this.prev_target_mouse_y != this.target_mouse_y) { double mouse_x = prev_target_mouse_x + (this.target_mouse_x - this.prev_target_mouse_x) * client.getTickDelta() + 0.5; double mouse_y = prev_target_mouse_y + (this.target_mouse_y - this.prev_target_mouse_y) * client.getTickDelta() + 0.5; GLFW.glfwSetCursorPos(client.window.getHandle(), mouse_x, mouse_y); } } public void on_render(@NotNull MinecraftClient client) { if (client.currentScreen == null && (this.prev_target_yaw != this.target_yaw || this.prev_target_pitch != this.target_pitch)) { float rotation_yaw = (float) (client.player.prevYaw + (this.target_yaw - client.player.prevYaw) * client.getTickDelta()); float rotation_pitch = (float) (client.player.prevPitch + (this.target_pitch - client.player.prevPitch) * client.getTickDelta()); client.player.yaw = rotation_yaw; client.player.pitch = MathHelper.clamp(rotation_pitch, -90.F, 90.F); if (client.player.isRiding()) { client.player.getVehicle().copyPositionAndRotation(client.player); } } } public void on_screen_open(@NotNull MinecraftClient client, int window_width, int window_height) { if (client.currentScreen == null) { this.target_mouse_x = this.prev_target_mouse_x = (int) (window_width / 2.F); this.target_mouse_y = this.prev_target_mouse_y = (int) (window_height / 2.F); } } private void fetch_button_input(@NotNull MinecraftClient client) { ByteBuffer buffer = GLFW.glfwGetJoystickButtons(this.controller); if (buffer != null) { for (int i = 0; i < buffer.limit(); i++) { boolean btn_state = buffer.get() == (byte) 1; boolean previous_state = BUTTON_STATES.getOrDefault(i, false); if (btn_state != previous_state) { this.handle_button(client, i, btn_state ? 0 : 1, btn_state); if (btn_state) BUTTON_COOLDOWNS.put(i, 5); } else if (btn_state) { if (BUTTON_COOLDOWNS.getOrDefault(i, 0) == 0) { BUTTON_COOLDOWNS.put(i, 5); this.handle_button(client, i, 2, true); } } BUTTON_STATES.put(i, btn_state); if (this.config.is_jump_button(i)) this.last_a_state = btn_state; } } } private void fetch_axe_input(@NotNull MinecraftClient client) { FloatBuffer buffer = GLFW.glfwGetJoystickAxes(this.controller); if (buffer != null) { for (int i = 0; i < buffer.limit(); i++) { float value = buffer.get(); float abs_value = Math.abs(value); int state = value > this.config.get_dead_zone() ? 1 : (value < -this.config.get_dead_zone() ? 2 : 0); this.handle_axe(client, i, value, abs_value, state); } } } private void handle_button(@NotNull MinecraftClient client, int button, int action, boolean state) { if (action == 0) { // Handles RB and LB buttons. if (this.config.is_hotbar_left_button(button) || this.config.is_hotbar_right_button(button)) { this.handle_rb_lb(client, this.config.is_hotbar_right_button(button)); return; } // Handles when the player presses the Start button. if (this.config.is_start_button(button)) { // If in game, then pause the game. if (client.currentScreen == null) client.openPauseMenu(false); else // Else just close the current screen. client.currentScreen.onClose(); return; } if (this.config.is_jump_button(button) && client.currentScreen != null) { if (this.action_gui_cooldown == 0) { Element focused = client.currentScreen.getFocused(); if (focused != null) this.handle_a_button(focused); this.action_gui_cooldown = 5; // Prevent to press too quickly the focused element, so we have to skip 5 ticks. return; } } } // Handles sneak button and continuous sneak. if (this.config.is_sneak_button(button) && client.player != null) { if (action == 0) { if (this.continuous_sneak) { this.set_sneaking(client, this.continuous_sneak = false); } else if (this.last_sneak > 3) { this.set_sneaking(client, this.continuous_sneak = true); } else { this.set_sneaking(client, true); this.last_sneak = 15; } } else if (action == 1) { if (this.continuous_sneak) return; this.set_sneaking(client, false); } return; } if (this.config.is_jump_button(button) && client.currentScreen != null) { if (this.last_a_state != state) { double mouse_x = client.mouse.getX() * (double) client.window.getScaledWidth() / (double) client.window.getWidth(); double mouse_y = client.mouse.getY() * (double) client.window.getScaledHeight() / (double) client.window.getHeight(); if (state) { client.currentScreen.mouseClicked(mouse_x, mouse_y, GLFW.GLFW_MOUSE_BUTTON_1); } else { client.currentScreen.mouseReleased(mouse_x, mouse_y, GLFW.GLFW_MOUSE_BUTTON_1); } return; } } if (client.currentScreen == null && action != 2) { Optional key_binding = this.config.get_keybind("button_" + button); key_binding.ifPresent(keyBinding -> ((LambdaKeyBinding) keyBinding).handle_press_state(action != 1)); } } private void handle_axe(@NotNull MinecraftClient client, int axe, float value, float abs_value, int state) { int as_button_state = value > 0.5F ? 1 : (value < -0.5F ? 2 : 0); double dead_zone = this.config.get_dead_zone(); if (client.currentScreen == null) { this.config.get_keybind("axe_" + axe + "+").ifPresent(key_binding -> ((LambdaKeyBinding) key_binding).handle_press_state(as_button_state == 1)); this.config.get_keybind("axe_" + axe + "-").ifPresent(key_binding -> ((LambdaKeyBinding) key_binding).handle_press_state(as_button_state == 2)); // Handles the look direction. if (this.config.is_look_axis(axe) && client.player != null) { if (this.config.is_view_down_control(axe, state)) { if (this.config.get_view_down_control().endsWith("+")) this.target_pitch = client.player.pitch + (this.config.get_rotation_speed() * (abs_value - dead_zone) / (1.0 - dead_zone)) * 0.33D; else this.target_pitch = client.player.pitch - (this.config.get_rotation_speed() * (abs_value + dead_zone) / (1.0 - dead_zone)) * 0.33D; this.target_pitch = MathHelper.clamp(this.target_pitch, -90.0D, 90.0D); } else if (this.config.is_view_up_control(axe, state)) { if (this.config.get_view_up_control().endsWith("+")) this.target_pitch = client.player.pitch + (this.config.get_rotation_speed() * (abs_value - dead_zone) / (1.0 - dead_zone)) * 0.33D; else this.target_pitch = client.player.pitch - (this.config.get_rotation_speed() * (abs_value + dead_zone) / (1.0 - dead_zone)) * 0.33D; this.target_pitch = MathHelper.clamp(this.target_pitch, -90.0D, 90.0D); } if (this.config.is_view_left_control(axe, state)) { if (this.config.get_view_left_control().endsWith("+")) this.target_yaw = client.player.yaw + (this.config.get_rotation_speed() * (abs_value - dead_zone) / (1.0 - dead_zone)) * 0.33D; else this.target_yaw = client.player.yaw - (this.config.get_rotation_speed() * (abs_value + dead_zone) / (1.0 - dead_zone)) * 0.33D; } else if (this.config.is_view_right_control(axe, state)) { if (this.config.get_view_right_control().endsWith("+")) this.target_yaw = client.player.yaw + (this.config.get_rotation_speed() * (abs_value - dead_zone) / (1.0 - dead_zone)) * 0.33D; else this.target_yaw = client.player.yaw - (this.config.get_rotation_speed() * (abs_value + dead_zone) / (1.0 - dead_zone)) * 0.33D; } } } else { boolean allow_mouse_control = true; if (this.action_gui_cooldown == 0 && this.config.is_movement_axis(axe)) { if (this.config.is_forward_button(axe, false, as_button_state)) { allow_mouse_control = this.change_focus(client.currentScreen, false); } else if (this.config.is_back_button(axe, false, as_button_state)) { allow_mouse_control = this.change_focus(client.currentScreen, true); } else if (this.config.is_left_button(axe, false, as_button_state)) { allow_mouse_control = this.handle_left_right(client.currentScreen, false); } else if (this.config.is_right_button(axe, false, as_button_state)) { allow_mouse_control = this.handle_left_right(client.currentScreen, true); } } float movement_x = 0.0F; float movement_y = 0.0F; if (this.config.is_back_button(axe, false, (value > 0 ? 1 : 2))) { movement_y = abs_value; } else if (this.config.is_forward_button(axe, false, (value > 0 ? 1 : 2))) { movement_y = -abs_value; } else if (this.config.is_left_button(axe, false, (value > 0 ? 1 : 2))) { movement_x = -abs_value; } else if (this.config.is_right_button(axe, false, (value > 0 ? 1 : 2))) { movement_x = abs_value; } if (client.currentScreen != null && allow_mouse_control) { boolean moving = Math.abs(movement_y) >= dead_zone || Math.abs(movement_x) >= dead_zone; if (moving) { /* Updates the target mouse position when the initial movement stick movement is detected. It prevents the cursor to jump to the old target mouse position if the user moves the cursor with the mouse. */ if (Math.abs(prev_x_axis) < dead_zone && Math.abs(prev_y_axis) < dead_zone) { double mouse_x = client.mouse.getX(); double mouse_y = client.mouse.getY(); prev_target_mouse_x = target_mouse_x = (int) mouse_x; prev_target_mouse_y = target_mouse_y = (int) mouse_y; } if (Math.abs(movement_x) >= dead_zone) this.mouse_speed_x = movement_x; else this.mouse_speed_x = 0.F; if (Math.abs(movement_y) >= dead_zone) this.mouse_speed_y = movement_y; else this.mouse_speed_y = 0.F; } else { this.mouse_speed_x = 0.F; this.mouse_speed_y = 0.F; } if (Math.abs(this.mouse_speed_x) > .05F || Math.abs(this.mouse_speed_y) > .05F) { this.target_mouse_x += this.mouse_speed_x * this.config.get_rotation_speed(); this.target_mouse_x = MathHelper.clamp(this.target_mouse_x, 0, client.window.getWidth()); this.target_mouse_y += this.mouse_speed_y * this.config.get_rotation_speed(); this.target_mouse_y = MathHelper.clamp(this.target_mouse_y, 0, client.window.getHeight()); } //this.move_mouse_to_closest_slot(client, client.currentScreen); } this.prev_x_axis = movement_x; this.prev_y_axis = movement_y; } } /** * Handles the press on RB on LB. * * @param client The client's instance. * @param right True if RB is pressed, else false. */ private void handle_rb_lb(@NotNull MinecraftClient client, boolean right) { // When ingame if (client.currentScreen == null) { if (right) client.player.inventory.selectedSlot = client.player.inventory.selectedSlot == 8 ? 0 : client.player.inventory.selectedSlot + 1; else client.player.inventory.selectedSlot = client.player.inventory.selectedSlot == 0 ? 8 : client.player.inventory.selectedSlot - 1; } else if (client.currentScreen instanceof CreativeInventoryScreen) { CreativeInventoryScreenAccessor creative_inventory = (CreativeInventoryScreenAccessor) client.currentScreen; int current_selected_tab = creative_inventory.get_selected_tab(); int next_tab = current_selected_tab + (right ? 1 : -1); if (next_tab < 0) next_tab = ItemGroup.GROUPS.length - 1; else if (next_tab >= ItemGroup.GROUPS.length) next_tab = 0; creative_inventory.set_selected_tab(ItemGroup.GROUPS[next_tab]); } } private void handle_a_button(@NotNull Element focused) { if (focused instanceof AbstractPressableButtonWidget) { AbstractPressableButtonWidget button_widget = (AbstractPressableButtonWidget) focused; button_widget.playDownSound(MinecraftClient.getInstance().getSoundManager()); button_widget.onPress(); } else if (focused instanceof WorldListWidget) { WorldListWidget list = (WorldListWidget) focused; list.method_20159().ifPresent(WorldListWidget.LevelItem::play); } else if (focused instanceof ParentElement) { Element child_focused = ((ParentElement) focused).getFocused(); if (child_focused != null) this.handle_a_button(child_focused); } } /** * Handles the left and right buttons. * * @param screen The current screen. * @param right True if the right button is pressed, else false. */ private boolean handle_left_right(@NotNull Screen screen, boolean right) { Element focused = screen.getFocused(); if (focused != null) return this.handle_right_left_element(focused, right); return true; } private boolean handle_right_left_element(@NotNull Element element, boolean right) { if (element instanceof SliderWidget) { SliderWidget slider = (SliderWidget) element; slider.keyPressed(right ? 262 : 263, 0, 0); this.action_gui_cooldown = 2; // Prevent to press too quickly the focused element, so we have to skip 5 ticks. return false; } else if (element instanceof ParentElement) { ParentElement entry_list = (ParentElement) element; Element focused = entry_list.getFocused(); if (focused == null) return true; return this.handle_right_left_element(focused, right); } return true; } /** * Sets if the player is sneaking. * * @param client The client's instance. * @param sneaking True if the player is sneaking, else false. */ private void set_sneaking(@NotNull MinecraftClient client, boolean sneaking) { ((LambdaKeyBinding) client.options.keySneak).handle_press_state(sneaking); } private boolean change_focus(@NotNull Screen screen, boolean down) { if (!screen.changeFocus(down)) { if (screen.changeFocus(down)) { this.action_gui_cooldown = 5; return false; } return true; } else { this.action_gui_cooldown = 5; return false; } } // Inspired from https://github.com/MrCrayfish/Controllable/blob/1.14.X/src/main/java/com/mrcrayfish/controllable/client/ControllerInput.java#L686. private void move_mouse_to_closest_slot(@NotNull MinecraftClient client, @Nullable Screen screen) { // Makes the mouse attracted to slots. This helps with selecting items when using a controller. if (screen instanceof AbstractContainerScreen) { AbstractContainerScreen inventory_screen = (AbstractContainerScreen) screen; AbstractContainerScreenAccessor accessor = (AbstractContainerScreenAccessor) inventory_screen; int gui_left = accessor.get_left(); int gui_top = accessor.get_top(); int mouse_x = (int) (target_mouse_x * (double) client.window.getScaledWidth() / (double) client.window.getWidth()); int mouse_y = (int) (target_mouse_y * (double) client.window.getScaledHeight() / (double) client.window.getHeight()); // Finds the closest slot in the GUI within 14 pixels. Optional> closest_slot = inventory_screen.getContainer().slotList.parallelStream() .map(slot -> { int pos_x = gui_left + slot.xPosition + 8; int pos_y = gui_top + slot.yPosition + 8; // Distance between the slot and the cursor. double distance = Math.sqrt(Math.pow(pos_x - mouse_x, 2) + Math.pow(pos_y - mouse_y, 2)); return Pair.of(slot, distance); }).filter(entry -> entry.get_value() <= 14.0) .min(Comparator.comparingDouble(Pair::get_value)); if (closest_slot.isPresent()) { Slot slot = closest_slot.get().get_key(); if (slot.hasStack() || !client.player.inventory.getMainHandStack().isEmpty()) { int slot_center_x_scaled = gui_left + slot.xPosition + 8; int slot_center_y_scaled = gui_top + slot.yPosition + 8; int slot_center_x = (int) (slot_center_x_scaled / ((double) client.window.getScaledWidth() / (double) client.window.getWidth())); int slot_center_y = (int) (slot_center_y_scaled / ((double) client.window.getScaledHeight() / (double) client.window.getHeight())); double delta_x = slot_center_x - target_mouse_x; double delta_y = slot_center_y - target_mouse_y; if (mouse_x != slot_center_x_scaled || mouse_y != slot_center_y_scaled) { this.target_mouse_x += delta_x * 0.75; this.target_mouse_y += delta_y * 0.75; } else { mouse_speed_x = 0.F; mouse_speed_y = 0.F; } this.mouse_speed_x *= .75F; this.mouse_speed_y *= .75F; } } else { this.mouse_speed_x *= .1F; this.mouse_speed_y *= .1F; } } else { this.mouse_speed_x = 0.F; this.mouse_speed_y = 0.F; } } }