/* * Copyright © 2021 LambdAurora * * This file is part of midnightcontrols. * * Licensed under the MIT license. For more information, * see the LICENSE file. */ package eu.midnightdust.midnightcontrols.client; import com.google.common.collect.ImmutableSet; import eu.midnightdust.lib.util.PlatformFunctions; import eu.midnightdust.midnightcontrols.client.util.storage.AxisStorage; import net.minecraft.client.gui.screen.option.KeybindsScreen; import net.minecraft.entity.vehicle.BoatEntity; import net.minecraft.util.Pair; import org.thinkingstudio.obsidianui.widget.AbstractSpruceWidget; import org.thinkingstudio.obsidianui.widget.container.SpruceEntryListWidget; import eu.midnightdust.midnightcontrols.MidnightControls; import eu.midnightdust.midnightcontrols.client.compat.*; import eu.midnightdust.midnightcontrols.client.controller.ButtonBinding; import eu.midnightdust.midnightcontrols.client.controller.Controller; import eu.midnightdust.midnightcontrols.client.controller.InputManager; import eu.midnightdust.midnightcontrols.client.enums.CameraMode; import eu.midnightdust.midnightcontrols.client.gui.RingScreen; import eu.midnightdust.midnightcontrols.client.touch.gui.TouchscreenOverlay; import eu.midnightdust.midnightcontrols.client.gui.widget.ControllerControlsWidget; import eu.midnightdust.midnightcontrols.client.mixin.*; import eu.midnightdust.midnightcontrols.client.ring.RingPage; import eu.midnightdust.midnightcontrols.client.util.HandledScreenAccessor; import eu.midnightdust.midnightcontrols.client.util.MathUtil; import org.thinkingstudio.obsidianui.navigation.NavigationDirection; import org.thinkingstudio.obsidianui.screen.SpruceScreen; import org.thinkingstudio.obsidianui.widget.AbstractSprucePressableButtonWidget; import org.thinkingstudio.obsidianui.widget.SpruceElement; import org.thinkingstudio.obsidianui.widget.SpruceLabelWidget; import org.thinkingstudio.obsidianui.widget.container.SpruceParentWidget; import eu.midnightdust.midnightcontrols.client.enums.ButtonState; 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.advancement.AdvancementTab; import net.minecraft.client.gui.screen.advancement.AdvancementsScreen; import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.client.gui.screen.ingame.MerchantScreen; import net.minecraft.client.gui.screen.ingame.StonecutterScreen; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.gui.screen.multiplayer.MultiplayerServerListWidget; import net.minecraft.client.gui.screen.world.WorldListWidget; import net.minecraft.client.gui.widget.*; import net.minecraft.screen.slot.Slot; import net.minecraft.text.TranslatableTextContent; import net.minecraft.util.math.MathHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWGamepadState; import java.util.*; import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client; import static org.lwjgl.glfw.GLFW.*; /** * Represents the midnightcontrols' input handler. * * @author LambdAurora * @version 1.7.0 * @since 1.0.0 */ public class MidnightInput { private static final Map BUTTON_COOLDOWNS = new HashMap<>(); // Cooldowns public int actionGuiCooldown = 0; public int joystickCooldown = 0; public boolean ignoreNextARelease = false; public boolean ignoreNextXRelease = false; private double targetYaw = 0.0; private double targetPitch = 0.0; private float prevXAxis = 0.f; private float prevYAxis = 0.f; private int targetMouseX = 0; private int targetMouseY = 0; private float mouseSpeedX = 0.f; private float mouseSpeedY = 0.f; public int inventoryInteractionCooldown = 0; public int screenCloseCooldown = 0; private ControllerControlsWidget controlsInput = null; public MidnightInput() {} /** * This method is called every Minecraft tick. * * @param client the client instance */ public void tick(@NotNull MinecraftClient client) { this.targetYaw = 0.F; this.targetPitch = 0.F; // Handles the key bindings. if (MidnightControlsClient.BINDING_LOOK_UP.isPressed()) { this.handleFlatLook(new AxisStorage(GLFW_GAMEPAD_AXIS_RIGHT_Y, 0.8F, 2)); } else if (MidnightControlsClient.BINDING_LOOK_DOWN.isPressed()) { this.handleFlatLook(new AxisStorage(GLFW_GAMEPAD_AXIS_RIGHT_Y, 0.8F, 1)); } if (MidnightControlsClient.BINDING_LOOK_LEFT.isPressed()) { this.handleFlatLook(new AxisStorage(GLFW_GAMEPAD_AXIS_RIGHT_X, 0.8F, 2)); } else if (MidnightControlsClient.BINDING_LOOK_RIGHT.isPressed()) { this.handleFlatLook(new AxisStorage(GLFW_GAMEPAD_AXIS_RIGHT_X, 0.8F, 1)); } InputManager.INPUT_MANAGER.tick(client); } /** * This method is called every Minecraft tick for controller input update. * * @param client the client instance */ public void tickController(@NotNull MinecraftClient client) { BUTTON_COOLDOWNS.entrySet().stream().filter(entry -> entry.getValue() > 0) .forEach(entry -> BUTTON_COOLDOWNS.put(entry.getKey(), entry.getValue() - 1)); // Decreases the cooldown for GUI actions. if (this.actionGuiCooldown > 0) --this.actionGuiCooldown; if (this.screenCloseCooldown > 0) --this.screenCloseCooldown; if (this.joystickCooldown > 0) --this.joystickCooldown; InputManager.updateStates(); var controller = MidnightControlsConfig.getController(); if (controller.isConnected()) { var state = controller.getState(); this.fetchButtonInput(client, state, false); this.fetchAxeInput(client, state, false); } MidnightControlsConfig.getSecondController().filter(Controller::isConnected) .ifPresent(joycon -> { GLFWGamepadState state = joycon.getState(); this.fetchButtonInput(client, state, true); this.fetchAxeInput(client, state, true); }); boolean allowInput = this.controlsInput == null || this.controlsInput.focusedBinding == null; if (allowInput) InputManager.updateBindings(client); if (this.controlsInput != null) { InputManager.STATES.forEach((num, button) -> { if (button.isPressed()) System.out.println(num); }); } if (this.controlsInput != null && InputManager.STATES.int2ObjectEntrySet().parallelStream().map(Map.Entry::getValue).allMatch(ButtonState::isUnpressed)) { if (MidnightControlsConfig.debug) MidnightControls.log("Finished MidnightInput Button Edit"); if (this.controlsInput.focusedBinding != null && !this.controlsInput.waiting) { int[] buttons = new int[this.controlsInput.currentButtons.size()]; for (int i = 0; i < this.controlsInput.currentButtons.size(); i++) buttons[i] = this.controlsInput.currentButtons.get(i); this.controlsInput.finishBindingEdit(buttons); this.controlsInput = null; } } if (this.inventoryInteractionCooldown > 0) this.inventoryInteractionCooldown--; } /** * This method is called before the screen is rendered. * * @param client the client instance * @param screen the screen to render */ public void onPreRenderScreen(@NotNull MinecraftClient client, @NotNull Screen screen) { if (!isScreenInteractive(screen)) { InputManager.INPUT_MANAGER.updateMousePosition(client); } } /** * This method is called to update the camera * * @param client the client instance */ public void updateCamera(@NotNull MinecraftClient client) { if (!(client.currentScreen == null || client.currentScreen instanceof TouchscreenOverlay)) return; var player = client.player; if (player == null) return; if (this.targetYaw != 0.f || this.targetPitch != 0.f) { float rotationYaw = (float) (client.player.prevYaw + (this.targetYaw * 0.175)); float rotationPitch = (float) (client.player.prevPitch + (this.targetPitch * 0.175)); client.player.prevYaw = rotationYaw; client.player.prevPitch = MathHelper.clamp(rotationPitch, -90.f, 90.f); client.player.setYaw(rotationYaw); client.player.setPitch(MathHelper.clamp(rotationPitch, -90.f, 90.f)); if (client.player.isRiding() && client.player.getVehicle() != null) { client.player.getVehicle().onPassengerLookAround(client.player); } client.getTutorialManager().onUpdateMouse(this.targetPitch, this.targetYaw); MidnightControlsCompat.handleCamera(client, targetYaw, targetPitch); } } /** * This method is called when a Screen is opened. * * @param client the client instance * @param windowWidth the window width * @param windowHeight the window height */ public void onScreenOpen(@NotNull MinecraftClient client, int windowWidth, int windowHeight) { if (client.currentScreen == null) { this.mouseSpeedX = this.mouseSpeedY = 0.0F; InputManager.INPUT_MANAGER.resetMousePosition(windowWidth, windowHeight); } else if (isScreenInteractive(client.currentScreen) && MidnightControlsConfig.virtualMouse) { ((MouseAccessor) client.mouse).midnightcontrols$onCursorPos(client.getWindow().getHandle(), 0, 0); InputManager.INPUT_MANAGER.resetMouseTarget(client); } this.inventoryInteractionCooldown = 5; } public void beginControlsInput(ControllerControlsWidget widget) { this.controlsInput = widget; if (widget != null) { this.controlsInput.currentButtons.clear(); this.controlsInput.waiting = true; } } private void fetchButtonInput(@NotNull MinecraftClient client, @NotNull GLFWGamepadState gamepadState, boolean leftJoycon) { var buffer = gamepadState.buttons(); for (int i = 0; i < buffer.limit(); i++) { int btn = leftJoycon ? ButtonBinding.controller2Button(i) : i; boolean btnState = buffer.get() == (byte) 1; var state = ButtonState.NONE; var previousState = InputManager.STATES.getOrDefault(btn, ButtonState.NONE); if (btnState != previousState.isPressed()) { state = btnState ? ButtonState.PRESS : ButtonState.RELEASE; this.handleButton(client, btn, btnState ? 0 : 1, btnState); if (btnState) BUTTON_COOLDOWNS.put(btn, 5); } else if (btnState) { state = ButtonState.REPEAT; if (BUTTON_COOLDOWNS.getOrDefault(btn, 0) == 0) { BUTTON_COOLDOWNS.put(btn, 5); this.handleButton(client, btn, 2, true); } } InputManager.STATES.put(btn, state); } } final MathUtil.PolarUtil polarUtil = new MathUtil.PolarUtil(); private void fetchAxeInput(@NotNull MinecraftClient client, @NotNull GLFWGamepadState gamepadState, boolean leftJoycon) { var buffer = gamepadState.axes(); polarUtil.calculate(buffer.get(GLFW_GAMEPAD_AXIS_LEFT_X), buffer.get(GLFW_GAMEPAD_AXIS_LEFT_Y), 1, MidnightControlsConfig.leftDeadZone); float leftX = polarUtil.polarX; float leftY = polarUtil.polarY; polarUtil.calculate(buffer.get(GLFW_GAMEPAD_AXIS_RIGHT_X), buffer.get(GLFW_GAMEPAD_AXIS_RIGHT_Y), 1, MidnightControlsConfig.rightDeadZone); float rightX = polarUtil.polarX; float rightY = polarUtil.polarY; boolean isRadialMenu = client.currentScreen instanceof RingScreen || (MidnightControlsCompat.isEmotecraftPresent() && EmotecraftCompat.isEmotecraftScreen(client.currentScreen)); for (int i = 0; i < buffer.limit(); i++) { int axis = leftJoycon ? ButtonBinding.controller2Button(i) : i; float value = buffer.get(); switch (i) { case GLFW_GAMEPAD_AXIS_LEFT_X -> {if (MidnightControlsConfig.analogMovement) value = leftX;} case GLFW_GAMEPAD_AXIS_LEFT_Y -> {if (MidnightControlsConfig.analogMovement) value = leftY;} case GLFW_GAMEPAD_AXIS_RIGHT_X -> value = rightX; case GLFW_GAMEPAD_AXIS_RIGHT_Y -> value = rightY; } float absValue = Math.abs(value); if (i == GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y) value *= -1.0F; int state = value > MidnightControlsConfig.rightDeadZone ? 1 : (value < -MidnightControlsConfig.rightDeadZone ? 2 : 0); if (!isRadialMenu) this.handleAxe(client, new AxisStorage(axis, value, absValue, state)); } if (isRadialMenu) { float x = leftX; float y = leftY; if (x == 0 && y == 0) { x = rightX; y = rightY; } int index = -1; float border = 0.3f; if (x < -border) { index = 3; if (y < -border) index = 0; else if (y > border) index = 5; } else if (x > border) { index = 4; if (y < -border) index = 2; else if (y > border) index = 7; } else { if (y < -border) index = 1; else if (y > border) index = 6; } if (client.currentScreen instanceof RingScreen && index > -1) RingPage.selected = index; if (MidnightControlsCompat.isEmotecraftPresent() && EmotecraftCompat.isEmotecraftScreen(client.currentScreen)) EmotecraftCompat.handleEmoteSelector(index); } } public void handleButton(@NotNull MinecraftClient client, int button, int action, boolean state) { if (this.controlsInput != null && this.controlsInput.focusedBinding != null) { if (action == 0 && !this.controlsInput.currentButtons.contains(button)) { this.controlsInput.currentButtons.add(button); var buttons = new int[this.controlsInput.currentButtons.size()]; for (int i = 0; i < this.controlsInput.currentButtons.size(); i++) buttons[i] = this.controlsInput.currentButtons.get(i); this.controlsInput.focusedBinding.setButton(buttons); this.controlsInput.waiting = false; } return; } if (client.currentScreen != null && (action == 0 || action == 2) && button == GLFW_GAMEPAD_BUTTON_Y && MidnightControlsConfig.arrowScreens.contains(client.currentScreen.getClass().getCanonicalName())) { pressKeyboardKey(client, GLFW.GLFW_KEY_ENTER); this.screenCloseCooldown = 5; } else if (action == 0 || action == 2) { if (client.currentScreen != null && (button == GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP || button == GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN || button == GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT || button == GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT)) { if (this.actionGuiCooldown == 0) { switch (button) { case GLFW_GAMEPAD_BUTTON_DPAD_UP -> this.changeFocus(client.currentScreen, NavigationDirection.UP); case GLFW_GAMEPAD_BUTTON_DPAD_DOWN -> this.changeFocus(client.currentScreen, NavigationDirection.DOWN); case GLFW_GAMEPAD_BUTTON_DPAD_LEFT -> this.handleLeftRight(client.currentScreen, false); case GLFW_GAMEPAD_BUTTON_DPAD_RIGHT -> this.handleLeftRight(client.currentScreen, true); } if (MidnightControlsConfig.wasdScreens.contains(client.currentScreen.getClass().getCanonicalName())) { switch (button) { case GLFW_GAMEPAD_BUTTON_DPAD_UP -> pressKeyboardKey(client, GLFW.GLFW_KEY_W); case GLFW_GAMEPAD_BUTTON_DPAD_DOWN -> pressKeyboardKey(client, GLFW.GLFW_KEY_S); case GLFW_GAMEPAD_BUTTON_DPAD_LEFT -> pressKeyboardKey(client, GLFW.GLFW_KEY_A); case GLFW_GAMEPAD_BUTTON_DPAD_RIGHT -> pressKeyboardKey(client, GLFW.GLFW_KEY_D); } } } return; } } if (action == 1) { if (button == GLFW.GLFW_GAMEPAD_BUTTON_A && client.currentScreen != null) { if (this.actionGuiCooldown == 0) { var focused = client.currentScreen.getFocused(); if (focused != null && isScreenInteractive(client.currentScreen)) { if (this.handleAButton(client.currentScreen, focused)) { this.actionGuiCooldown = 5; // Set the cooldown to 5 ticks to avoid unintended button presses. return; } } else if (PlatformFunctions.isModLoaded("libgui")) LibGuiCompat.handlePress(client.currentScreen); } } } if (button == GLFW.GLFW_GAMEPAD_BUTTON_A && client.currentScreen != null && !isScreenInteractive(client.currentScreen) && this.actionGuiCooldown == 0) { if (client.currentScreen instanceof HandledScreen handledScreen && ((HandledScreenAccessor) handledScreen).midnightcontrols$getSlotAt( client.mouse.getX() * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth(), client.mouse.getY() * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight()) != null) return; if (!this.ignoreNextARelease && client.currentScreen != null) { var accessor = (MouseAccessor) client.mouse; accessor.midnightcontrols$onCursorPos(client.getWindow().getHandle(), client.mouse.getX(), client.mouse.getY()); if (action == 0) { // Button pressed accessor.midnightcontrols$onMouseButton(client.getWindow().getHandle(), GLFW_MOUSE_BUTTON_LEFT, 1, 0); } else if (action == 1) { // Button released accessor.midnightcontrols$onMouseButton(client.getWindow().getHandle(), GLFW_MOUSE_BUTTON_LEFT, 0, 0); client.currentScreen.setDragging(false); } else if (action == 2) { // Button held down / dragging client.currentScreen.setDragging(true); } this.screenCloseCooldown = 5; } else { this.ignoreNextARelease = false; } } else if (button == GLFW.GLFW_GAMEPAD_BUTTON_X && client.currentScreen != null && !isScreenInteractive(client.currentScreen) && this.actionGuiCooldown == 0) { if (client.currentScreen instanceof HandledScreen handledScreen && ((HandledScreenAccessor) handledScreen).midnightcontrols$getSlotAt( client.mouse.getX() * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth(), client.mouse.getY() * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight()) != null) return; if (!this.ignoreNextXRelease && client.currentScreen != null) { double mouseX = client.mouse.getX() * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth(); double mouseY = client.mouse.getY() * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight(); if (action == 0) { Screen.wrapScreenError(() -> client.currentScreen.mouseClicked(mouseX, mouseY, GLFW.GLFW_MOUSE_BUTTON_2), "mouseClicked event handler", client.currentScreen.getClass().getCanonicalName()); } else if (action == 1) { Screen.wrapScreenError(() -> client.currentScreen.mouseReleased(mouseX, mouseY, GLFW.GLFW_MOUSE_BUTTON_2), "mouseReleased event handler", client.currentScreen.getClass().getCanonicalName()); } this.screenCloseCooldown = 5; } else { this.ignoreNextXRelease = false; } } } private void handleAxe(@NotNull MinecraftClient client, AxisStorage storage) { this.setCurrentPolarities(storage); this.handleMovement(client, storage); if (this.handleScreenScrolling(client.currentScreen, storage)) return; storage.absValue = (float) MathHelper.clamp(storage.absValue / MidnightControlsConfig.getAxisMaxValue(storage.axis), 0.f, 1.f); if (client.currentScreen == null) { // Handles the look direction. this.handleLook(client, storage); } else { boolean allowMouseControl = true; if (this.actionGuiCooldown == 0 && MidnightControlsConfig.isMovementAxis(storage.axis) && isScreenInteractive(client.currentScreen)) { if (MidnightControlsConfig.isForwardButton(storage.axis, false, storage.asButtonState)) { allowMouseControl = this.changeFocus(client.currentScreen, NavigationDirection.UP); } else if (MidnightControlsConfig.isBackButton(storage.axis, false, storage.asButtonState)) { allowMouseControl = this.changeFocus(client.currentScreen, NavigationDirection.DOWN); } else if (MidnightControlsConfig.isLeftButton(storage.axis, false, storage.asButtonState)) { allowMouseControl = this.handleLeftRight(client.currentScreen, false); } else if (MidnightControlsConfig.isRightButton(storage.axis, false, storage.asButtonState)) { allowMouseControl = this.handleLeftRight(client.currentScreen, true); } } float movementX = 0.f; float movementY = 0.f; if (MidnightControlsConfig.isBackButton(storage.axis, false, (storage.value > 0 ? 1 : 2))) { movementY = storage.absValue; } else if (MidnightControlsConfig.isForwardButton(storage.axis, false, (storage.value > 0 ? 1 : 2))) { movementY = -storage.absValue; } else if (MidnightControlsConfig.isLeftButton(storage.axis, false, (storage.value > 0 ? 1 : 2))) { movementX = -storage.absValue; } else if (MidnightControlsConfig.isRightButton(storage.axis, false, (storage.value > 0 ? 1 : 2))) { movementX = storage.absValue; } if (client.currentScreen != null && allowMouseControl) { boolean moving = movementY != 0 || movementX != 0; 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(prevXAxis) < storage.deadZone && Math.abs(prevYAxis) < storage.deadZone) { InputManager.INPUT_MANAGER.resetMouseTarget(client); } this.mouseSpeedX = movementX; this.mouseSpeedY = movementY; } else { this.mouseSpeedX = 0.f; this.mouseSpeedY = 0.f; } if (Math.abs(this.mouseSpeedX) >= .05f || Math.abs(this.mouseSpeedY) >= .05f) { InputManager.queueMoveMousePosition( this.mouseSpeedX * MidnightControlsConfig.mouseSpeed, this.mouseSpeedY * MidnightControlsConfig.mouseSpeed ); } this.moveMouseToClosestSlot(client, client.currentScreen); } this.prevXAxis = movementX; this.prevYAxis = movementY; } } private void setCurrentPolarities(AxisStorage storage) { boolean currentPlusState = storage.value > storage.deadZone; boolean currentMinusState = storage.value < -storage.deadZone; if (storage.axis == GLFW_GAMEPAD_AXIS_LEFT_TRIGGER || storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) currentMinusState = false; if (!MidnightControlsConfig.analogMovement && (storage.axis == GLFW_GAMEPAD_AXIS_LEFT_X || storage.axis == GLFW_GAMEPAD_AXIS_LEFT_Y)) { currentPlusState = storage.asButtonState == 1; currentMinusState = storage.asButtonState == 2; } var posButton = ButtonBinding.axisAsButton(storage.axis, true); var negButton = ButtonBinding.axisAsButton(storage.axis, false); var previousPlusState = InputManager.STATES.getOrDefault(posButton, ButtonState.NONE); var previousMinusState = InputManager.STATES.getOrDefault(negButton, ButtonState.NONE); if (currentPlusState != previousPlusState.isPressed()) { InputManager.STATES.put(posButton, currentPlusState ? ButtonState.PRESS : ButtonState.RELEASE); if (currentPlusState) BUTTON_COOLDOWNS.put(posButton, 5); } else if (currentPlusState) { InputManager.STATES.put(posButton, ButtonState.REPEAT); if (BUTTON_COOLDOWNS.getOrDefault(posButton, 0) == 0) { BUTTON_COOLDOWNS.put(posButton, 5); } } if (currentMinusState != previousMinusState.isPressed()) { InputManager.STATES.put(negButton, currentMinusState ? ButtonState.PRESS : ButtonState.RELEASE); if (currentMinusState) BUTTON_COOLDOWNS.put(negButton, 5); } else if (currentMinusState) { InputManager.STATES.put(negButton, ButtonState.REPEAT); if (BUTTON_COOLDOWNS.getOrDefault(negButton, 0) == 0) { BUTTON_COOLDOWNS.put(negButton, 5); } } storage.polarity = currentPlusState ? AxisStorage.Polarity.PLUS : currentMinusState ? AxisStorage.Polarity.MINUS : AxisStorage.Polarity.ZERO; } private void handleMovement(@NotNull MinecraftClient client, AxisStorage storage) { float axisValue = storage.absValue; if (!MidnightControlsConfig.analogMovement || (client.player != null && client.player.getVehicle() instanceof BoatEntity)) { axisValue = (float) (storage.absValue - storage.deadZone); axisValue /= (float) (1.0 - storage.deadZone); axisValue *= (float) storage.deadZone; } axisValue = (float) Math.min(axisValue / MidnightControlsConfig.getAxisMaxValue(storage.axis), 1); InputManager.BUTTON_VALUES.put(ButtonBinding.axisAsButton(storage.axis, true), storage.polarity == AxisStorage.Polarity.PLUS ? axisValue : 0.f); InputManager.BUTTON_VALUES.put(ButtonBinding.axisAsButton(storage.axis, false), storage.polarity == AxisStorage.Polarity.MINUS ? axisValue : 0.f); } private boolean handleScreenScrolling(Screen screen, AxisStorage storage) { if (storage.axis > GLFW_GAMEPAD_AXIS_RIGHT_Y) return false; // @TODO allow rebinding to left stick int preferredAxis = true ? GLFW_GAMEPAD_AXIS_RIGHT_Y : GLFW_GAMEPAD_AXIS_LEFT_Y; if (this.controlsInput != null && this.controlsInput.focusedBinding != null) { if (storage.asButtonState != 0 && !this.controlsInput.currentButtons.contains(ButtonBinding.axisAsButton(storage.axis, storage.asButtonState == 1))) { this.controlsInput.currentButtons.add(ButtonBinding.axisAsButton(storage.axis, storage.asButtonState == 1)); int[] buttons = new int[this.controlsInput.currentButtons.size()]; for (int i = 0; i < this.controlsInput.currentButtons.size(); i++) buttons[i] = this.controlsInput.currentButtons.get(i); this.controlsInput.focusedBinding.setButton(buttons); this.controlsInput.waiting = false; } return true; } else if (storage.absValue >= storage.deadZone) { if (screen instanceof CreativeInventoryScreen creativeInventoryScreen) { if (storage.axis == preferredAxis) { var accessor = (CreativeInventoryScreenAccessor) creativeInventoryScreen; if (accessor.midnightcontrols$hasScrollbar() && storage.absValue >= storage.deadZone) { creativeInventoryScreen.mouseScrolled(0.0, 0.0, 0, -storage.value); } return true; } } else if (screen instanceof MerchantScreen || screen instanceof StonecutterScreen) { if (storage.axis == preferredAxis) { screen.mouseScrolled(0.0, 0.0, 0, -(storage.value * 1.5f)); return true; } } else if (screen instanceof AdvancementsScreen advancementsScreen) { if (storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_X || storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_Y) { var accessor = (AdvancementsScreenAccessor) advancementsScreen; AdvancementTab tab = accessor.getSelectedTab(); tab.move(storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_X ? -storage.value * 5.0 : 0.0, storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_Y ? -storage.value * 5.0 : 0.0); return true; } } else if (screen != null) { if (storage.axis == preferredAxis && !handleListWidgetScrolling(screen.children(), storage.value)) { screen.mouseScrolled(0.0, 0.0, 0, -(storage.value * 1.5f)); } else if (isScreenInteractive(screen)) { if (joystickCooldown == 0) { switch (storage.axis) { case GLFW_GAMEPAD_AXIS_LEFT_Y -> { this.changeFocus(screen, storage.value > 0 ? NavigationDirection.UP : NavigationDirection.DOWN); joystickCooldown = 4; } case GLFW_GAMEPAD_AXIS_LEFT_X -> { this.handleLeftRight(screen, storage.value > 0); joystickCooldown = 4; } } } return true; } } } return false; } public boolean handleListWidgetScrolling(List children, float value) { return children.stream().filter(element -> element instanceof SpruceEntryListWidget) .map(element -> (SpruceEntryListWidget) element) .filter(AbstractSpruceWidget::isFocusedOrHovered) .anyMatch(element -> { element.mouseScrolled(0.0, 0.0, 0, -value); return true; }) || children.stream().filter(element -> element instanceof EntryListWidget) .map(element -> (EntryListWidget) element) .filter(element -> element.getType().isFocused()) .anyMatch(element -> { element.mouseScrolled(0.0, 0.0, 0, -value); return true; }); } public boolean handleAButton(@NotNull Screen screen, @NotNull Element focused) { if (focused instanceof PressableWidget widget) { widget.playDownSound(MinecraftClient.getInstance().getSoundManager()); widget.onPress(); return true; } else if (focused instanceof AbstractSprucePressableButtonWidget widget) { widget.playDownSound(); widget.onPress(); return true; } else if (focused instanceof SpruceLabelWidget labelWidget) { labelWidget.onPress(); return true; } else if (focused instanceof WorldListWidget list) { list.getSelectedAsOptional().ifPresent(WorldListWidget.WorldEntry::play); return true; } else if (focused instanceof MultiplayerServerListWidget list) { var entry = list.getSelectedOrNull(); if (entry instanceof MultiplayerServerListWidget.LanServerEntry || entry instanceof MultiplayerServerListWidget.ServerEntry) { ((MultiplayerScreen) screen).select(entry); ((MultiplayerScreen) screen).connect(); } } else if (focused instanceof SpruceParentWidget) { var childFocused = ((SpruceParentWidget) focused).getFocused(); if (childFocused != null) return this.handleAButton(screen, childFocused); } else if (focused instanceof ParentElement widget) { var childFocused = widget.getFocused(); if (childFocused != null) return this.handleAButton(screen, childFocused); } else if (PlatformFunctions.isModLoaded("yet-another-config-lib") && YACLCompat.handleAButton(screen, focused)) { return true; } else pressKeyboardKey(screen, GLFW_KEY_ENTER); return false; } /** * Handles the left and right buttons. * * @param screen the current screen * @param right true if the right button is pressed, else false */ private boolean handleLeftRight(@NotNull Screen screen, boolean right) { if (screen instanceof SpruceScreen spruceScreen) { spruceScreen.onNavigation(right ? NavigationDirection.RIGHT : NavigationDirection.LEFT, false); this.actionGuiCooldown = 5; return false; } if (PlatformFunctions.isModLoaded("yet-another-config-lib") && YACLCompat.handleLeftRight(screen, right)) { this.actionGuiCooldown = 5; return false; } var focused = screen.getFocused(); if (focused != null) if (this.handleRightLeftElement(focused, right)) return this.changeFocus(screen, right ? NavigationDirection.RIGHT : NavigationDirection.LEFT); return true; } private boolean handleRightLeftElement(@NotNull Element element, boolean right) { switch (element) { case SpruceElement spruceElement -> { if (spruceElement.requiresCursor()) return true; return !spruceElement.onNavigation(right ? NavigationDirection.RIGHT : NavigationDirection.LEFT, false); } case SliderWidget slider -> { if (slider.active) { slider.keyPressed(right ? 262 : 263, 0, 0); this.actionGuiCooldown = 2; // Prevent to press too quickly the focused element, so we have to skip 5 ticks. return true; } } // case AlwaysSelectedEntryListWidget alwaysSelectedEntryListWidget -> { // //TODO ((EntryListWidgetAccessor) element).midnightcontrols$moveSelection(right ? EntryListWidget.MoveDirection.DOWN : EntryListWidget.MoveDirection.UP); // return false; // } case ParentElement entryList -> { var focused = entryList.getFocused(); if (focused == null) return true; return this.handleRightLeftElement(focused, right); } default -> { } } return true; } private double prevX = 0; private double prevY = 0; private double xValue; private int xState; /** * Handles the look direction input. * * @param client the client instance * @param storage the state of the provided axis */ public void handleLook(@NotNull MinecraftClient client, AxisStorage storage) { if (client.player == null || !(storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_Y || storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_X)) return; // Handles the look direction. if (MidnightControlsConfig.cameraMode == CameraMode.FLAT) handleFlatLook(storage); else handleAdaptiveLook(storage); } private void handleFlatLook(AxisStorage storage) { if (storage.state != 0) { double rotation = Math.pow(storage.value, 2.0) * 0.11D * (storage.state == 2 ? -1 : 1); if (storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_Y) this.targetPitch = rotation * MidnightControlsConfig.getRightYAxisSign() * MidnightControlsConfig.yAxisRotationSpeed; else this.targetYaw = rotation * MidnightControlsConfig.getRightXAxisSign() * MidnightControlsConfig.rotationSpeed; } } private void handleAdaptiveLook(AxisStorage storage) { if (storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_X) { xValue = storage.value; xState = storage.state; } else { double yStep = (MidnightControlsConfig.yAxisRotationSpeed / 100) * 0.6000000238418579 + 0.20000000298023224; double xStep = (MidnightControlsConfig.rotationSpeed / 100) * 0.6000000238418579 + 0.20000000298023224; float yValue = storage.value; float yState = storage.state; double cursorDeltaX = 2 * xValue - this.prevX; double cursorDeltaY = 2 * yValue - this.prevY; boolean slowdown = client.options.getPerspective().isFirstPerson() && Objects.requireNonNull(client.player).isUsingSpyglass(); double x = cursorDeltaX * xStep * (slowdown ? xStep : 1); double y = cursorDeltaY * yStep * (slowdown ? yStep : 1); double powXValue = Math.pow(x, 2.0); double powYValue = Math.pow(y, 2.0); if (xState != 0) { double sign = MidnightControlsConfig.getRightXAxisSign() * MidnightControlsConfig.rotationSpeed; this.targetYaw = sign * powXValue * 0.11D * (xState == 2 ? -1 : 1); } if (yState != 0) { double sign = MidnightControlsConfig.getRightYAxisSign() * MidnightControlsConfig.yAxisRotationSpeed; this.targetPitch = sign * powYValue * 0.11D * (yState == 2 ? -1 : 1); } this.prevY = yValue; this.prevX = xValue; } } public void handleTouchscreenLook(AxisStorage storage) { if (storage.state != 0) { double rotation = storage.value * 0.11D * MidnightControlsConfig.touchSpeed/5; if (storage.axis == GLFW_GAMEPAD_AXIS_RIGHT_Y) this.targetPitch = rotation; else this.targetYaw = rotation; } } private boolean changeFocus(@NotNull Screen screen, NavigationDirection direction) { if (!isScreenInteractive(screen) && !screen.getClass().getCanonicalName().contains("me.jellysquid.mods.sodium.client.gui")) return false; try { if (screen instanceof SpruceScreen spruceScreen) { if (spruceScreen.onNavigation(direction, false)) { this.actionGuiCooldown = 5; } return true; } switch (direction) { case UP -> pressKeyboardKey(screen, GLFW.GLFW_KEY_UP); case DOWN -> pressKeyboardKey(screen, GLFW.GLFW_KEY_DOWN); case LEFT -> pressKeyboardKey(screen, GLFW.GLFW_KEY_LEFT); case RIGHT -> pressKeyboardKey(screen, GLFW.GLFW_KEY_RIGHT); } this.actionGuiCooldown = 5; return true; } catch (Exception exception) {MidnightControls.warn("Unknown exception encountered while trying to change focus: "+exception);} return false; } /** * Tries to go back. * * @param screen the current screen * @return true if successful, else false */ public boolean tryGoBack(@NotNull Screen screen) { var set = ImmutableSet.of("gui.back", "gui.done", "gui.cancel", "gui.toTitle", "gui.toMenu"); if (screen instanceof KeybindsScreen) return false; return screen.children().stream().filter(element -> element instanceof PressableWidget) .map(element -> (PressableWidget) element) .filter(element -> element.getMessage() != null && element.getMessage().getContent() != null) .anyMatch(element -> { if (element.getMessage().getContent() instanceof TranslatableTextContent translatableText) { if (set.stream().anyMatch(key -> translatableText.getKey().equals(key))) { element.onPress(); return true; } } return false; }); } public static boolean isScreenInteractive(@NotNull Screen screen) { return !(screen instanceof HandledScreen || MidnightControlsConfig.joystickAsMouse || MidnightControlsConfig.mouseScreens.stream().anyMatch(a -> screen.getClass().toString().contains(a)) || (screen instanceof SpruceScreen && ((SpruceScreen) screen).requiresCursor()) || MidnightControlsCompat.requireMouseOnScreen(screen)); } public void pressKeyboardKey(MinecraftClient client, int key) { client.keyboard.onKey(client.getWindow().getHandle(), key, 0, 1, 0); } public void pressKeyboardKey(Screen screen, int key) { screen.keyPressed(key, 0, 1); } // Inspired from https://github.com/MrCrayfish/Controllable/blob/1.14.X/src/main/java/com/mrcrayfish/controllable/client/ControllerInput.java#L686. private void moveMouseToClosestSlot(@NotNull MinecraftClient client, @Nullable Screen screen) { // Makes the mouse attracted to slots. This helps with selecting items when using a controller. if (screen instanceof HandledScreen inventoryScreen) { var accessor = (HandledScreenAccessor) inventoryScreen; int guiLeft = accessor.getX(); int guiTop = accessor.getY(); int mouseX = (int) (targetMouseX * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth()); int mouseY = (int) (targetMouseY * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight()); // Finds the closest slot in the GUI within 14 pixels. Optional> closestSlot = inventoryScreen.getScreenHandler().slots.parallelStream() .map(slot -> { int x = guiLeft + slot.x + 8; int y = guiTop + slot.y + 8; // Distance between the slot and the cursor. double distance = Math.sqrt(Math.pow(x - mouseX, 2) + Math.pow(y - mouseY, 2)); return new Pair(slot, distance); }).filter(entry -> entry.getRight() <= 14.0) .min(Comparator.comparingDouble(Pair::getRight)); if (closestSlot.isPresent() && client.player != null) { var slot = closestSlot.get().getLeft(); if (slot.hasStack() || !client.player.getInventory().getMainHandStack().isEmpty()) { int slotCenterXScaled = guiLeft + slot.x + 8; int slotCenterYScaled = guiTop + slot.y + 8; int slotCenterX = (int) (slotCenterXScaled / ((double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth())); int slotCenterY = (int) (slotCenterYScaled / ((double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight())); double deltaX = slotCenterX - targetMouseX; double deltaY = slotCenterY - targetMouseY; if (mouseX != slotCenterXScaled || mouseY != slotCenterYScaled) { this.targetMouseX += (int) (deltaX * 0.75); this.targetMouseY += (int) (deltaY * 0.75); } else { this.mouseSpeedX *= 0.3F; this.mouseSpeedY *= 0.3F; } this.mouseSpeedX *= .75F; this.mouseSpeedY *= .75F; } else { this.mouseSpeedX *= .1F; this.mouseSpeedY *= .1F; } } else { this.mouseSpeedX *= .3F; this.mouseSpeedY *= .3F; } } else { this.mouseSpeedX = 0.F; this.mouseSpeedY = 0.F; } } }