From aad9447325ba3e0dc36d86208ffa66c357a16773 Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 31 Jan 2023 21:41:38 +0000 Subject: [PATCH] in-game input and start of keybind system --- build.gradle.kts | 5 ++ gradle/libs.versions.toml | 2 + .../dev/isxander/controlify/Controlify.java | 47 ++++++++++++---- .../isxander/controlify/bindings/Bind.java | 37 ++++++++++++ .../bindings/ControllerBinding.java | 41 ++++++++++++++ .../bindings/ControllerBindings.java | 22 ++++++++ .../controlify/controller/Controller.java | 15 ++++- .../controlify/event/ControlifyEvents.java | 30 ++++++++++ .../ingame/ControllerPlayerMovement.java | 36 ++++++++++++ .../controlify/ingame/InGameInputHandler.java | 56 +++++++++++++++++++ .../mixins/AbstractSliderButtonMixin.java | 3 + .../mixins/ClientPacketListenerMixin.java | 35 ++++++++++++ .../controlify/mixins/MinecraftMixin.java | 5 ++ src/main/resources/controlify.mixins.json | 1 + src/main/resources/fabric.mod.json | 4 +- 15 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/bindings/Bind.java create mode 100644 src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java create mode 100644 src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java create mode 100644 src/main/java/dev/isxander/controlify/event/ControlifyEvents.java create mode 100644 src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java create mode 100644 src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/ClientPacketListenerMixin.java diff --git a/build.gradle.kts b/build.gradle.kts index fe25167..b4f690b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ repositories { includeGroup("maven.modrinth") } } + maven("https://jitpack.io") } val minecraftVersion = libs.versions.minecraft.get() @@ -39,6 +40,10 @@ dependencies { modImplementation(libs.fabric.loader) modImplementation(libs.fabric.api) + + implementation(libs.mixin.extras) + annotationProcessor(libs.mixin.extras) + include(libs.mixin.extras) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38b5847..96d9c06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,12 +11,14 @@ minecraft = "23w04a" quilt_mappings = "10" fabric_loader = "0.14.13" fabric_api = "0.73.1+1.19.4" +mixin_extras = "0.2.0-beta.1" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric_api" } +mixin_extras = { module = "com.github.llamalad7:mixinextras", version.ref = "mixin_extras" } [plugins] loom = { id = "fabric-loom", version.ref = "loom" } diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index afdab1e..70c8739 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -1,10 +1,10 @@ package dev.isxander.controlify; import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider; -import dev.isxander.controlify.controller.AxesState; -import dev.isxander.controlify.controller.ButtonState; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.event.ControlifyEvents; +import dev.isxander.controlify.ingame.InGameInputHandler; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.minecraft.client.Minecraft; import org.lwjgl.glfw.GLFW; @@ -13,13 +13,14 @@ public class Controlify { private static Controlify instance = null; private Controller currentController; + private InGameInputHandler inGameInputHandler; private InputMode currentInputMode; public void onInitializeInput() { // find already connected controllers for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) { if (GLFW.glfwJoystickPresent(i)) { - currentController = Controller.byId(i); + setCurrentController(Controller.byId(i)); System.out.println("Connected: " + currentController.name()); this.setCurrentInputMode(InputMode.CONTROLLER); } @@ -29,23 +30,21 @@ public class Controlify { GLFW.glfwSetJoystickCallback((jid, event) -> { System.out.println("Event: " + event); if (event == GLFW.GLFW_CONNECTED) { - currentController = Controller.byId(jid); + setCurrentController(Controller.byId(jid)); System.out.println("Connected: " + currentController.name()); this.setCurrentInputMode(InputMode.CONTROLLER); } else if (event == GLFW.GLFW_DISCONNECTED) { Controller.CONTROLLERS.remove(jid); - currentController = Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null); + setCurrentController(Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null)); System.out.println("Disconnected: " + jid); this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); } }); - ClientTickEvents.START_CLIENT_TICK.register(client -> { - updateControllers(); - }); + ClientTickEvents.START_CLIENT_TICK.register(this::updateControllers); } - public void updateControllers() { + public void updateControllers(Minecraft client) { for (Controller controller : Controller.CONTROLLERS.values()) { controller.updateState(); } @@ -55,8 +54,31 @@ public class Controlify { if (state.hasAnyInput()) this.setCurrentInputMode(InputMode.CONTROLLER); - Minecraft client = Minecraft.getInstance(); - if (client.screen != null && currentController != null) ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + if (currentController == null) { + this.setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + return; + } + + if (client.screen != null) { + ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + } else { + this.getInGameInputHandler().inputTick(); + } + } + + public Controller getCurrentController() { + return currentController; + } + + public void setCurrentController(Controller controller) { + if (this.currentController == controller) return; + + this.currentController = controller; + this.inGameInputHandler = new InGameInputHandler(controller); + } + + public InGameInputHandler getInGameInputHandler() { + return inGameInputHandler; } public InputMode getCurrentInputMode() { @@ -64,7 +86,10 @@ public class Controlify { } public void setCurrentInputMode(InputMode currentInputMode) { + if (this.currentInputMode == currentInputMode) return; + this.currentInputMode = currentInputMode; + ControlifyEvents.INPUT_MODE_CHANGED.invoker().onInputModeChanged(currentInputMode); } public static Controlify getInstance() { diff --git a/src/main/java/dev/isxander/controlify/bindings/Bind.java b/src/main/java/dev/isxander/controlify/bindings/Bind.java new file mode 100644 index 0000000..6ad0ff9 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/Bind.java @@ -0,0 +1,37 @@ +package dev.isxander.controlify.bindings; + +import dev.isxander.controlify.controller.ControllerState; + +@FunctionalInterface +public interface Bind { + boolean state(ControllerState controllerState); + + Bind A_BUTTON = state -> state.buttons().a(); + Bind B_BUTTON = state -> state.buttons().b(); + Bind X_BUTTON = state -> state.buttons().x(); + Bind Y_BUTTON = state -> state.buttons().y(); + Bind LEFT_BUMPER = state -> state.buttons().leftBumper(); + Bind RIGHT_BUMPER = state -> state.buttons().rightBumper(); + Bind LEFT_STICK = state -> state.buttons().leftStick(); + Bind RIGHT_STICK = state -> state.buttons().rightStick(); + Bind START = state -> state.buttons().start(); + Bind BACK = state -> state.buttons().back(); + Bind LEFT_TRIGGER = leftTrigger(0.5f); + Bind RIGHT_TRIGGER = rightTrigger(0.5f); + + Bind[] ALL = { + A_BUTTON, B_BUTTON, X_BUTTON, Y_BUTTON, + LEFT_BUMPER, RIGHT_BUMPER, + LEFT_STICK, RIGHT_STICK, + START, BACK, + LEFT_TRIGGER, RIGHT_TRIGGER + }; + + static Bind leftTrigger(float threshold) { + return state -> state.axes().leftTrigger() > threshold; + } + + static Bind rightTrigger(float threshold) { + return state -> state.axes().rightTrigger() > threshold; + } +} diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java new file mode 100644 index 0000000..bba9edb --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java @@ -0,0 +1,41 @@ +package dev.isxander.controlify.bindings; + +import dev.isxander.controlify.controller.Controller; +import net.minecraft.network.chat.Component; + +public class ControllerBinding { + private final Controller controller; + private final Bind bind; + private final Component name, description; + + public ControllerBinding(Controller controller, Bind defaultBind, String id, Component description) { + this.controller = controller; + this.bind = defaultBind; + this.name = Component.translatable("controlify.binding." + id); + this.description = description; + } + + public ControllerBinding(Controller controller, Bind defaultBind, String id) { + this(controller, defaultBind, id, Component.empty()); + } + + public boolean held() { + return bind.state(controller.state()); + } + + public boolean justPressed() { + return held() && !bind.state(controller.prevState()); + } + + public boolean justReleased() { + return !held() && bind.state(controller.prevState()); + } + + public Component name() { + return name; + } + + public Component description() { + return description; + } +} diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java new file mode 100644 index 0000000..1a6c82f --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -0,0 +1,22 @@ +package dev.isxander.controlify.bindings; + +import dev.isxander.controlify.controller.Controller; + +public class ControllerBindings { + public final ControllerBinding JUMP, SNEAK, ATTACK, USE, SPRINT, NEXT_SLOT, PREV_SLOT; + public final ControllerBinding[] ALL; + + public ControllerBindings(Controller controller) { + JUMP = new ControllerBinding(controller, Bind.A_BUTTON, "jump"); + SNEAK = new ControllerBinding(controller, Bind.RIGHT_STICK, "sneak"); + ATTACK = new ControllerBinding(controller, Bind.RIGHT_TRIGGER, "attack"); + USE = new ControllerBinding(controller, Bind.LEFT_TRIGGER, "use"); + SPRINT = new ControllerBinding(controller, Bind.LEFT_STICK, "sprint"); + NEXT_SLOT = new ControllerBinding(controller, Bind.RIGHT_BUMPER, "next_slot"); + PREV_SLOT = new ControllerBinding(controller, Bind.LEFT_BUMPER, "prev_slot"); + + ALL = new ControllerBinding[] { + JUMP, SNEAK, ATTACK, USE, SPRINT, NEXT_SLOT, PREV_SLOT + }; + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index 8f7faf8..ae864d5 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.controller; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import dev.isxander.controlify.bindings.ControllerBindings; +import dev.isxander.controlify.event.ControlifyEvents; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWGamepadState; @@ -10,13 +11,17 @@ import java.util.Objects; public final class Controller { public static final Map CONTROLLERS = new HashMap<>(); + private final int id; private final String guid; private final String name; private final boolean gamepad; + private ControllerState state = ControllerState.EMPTY; private ControllerState prevState = ControllerState.EMPTY; + private final ControllerBindings bindings = new ControllerBindings(this); + public Controller(int id, String guid, String name, boolean gamepad) { this.id = id; this.guid = guid; @@ -47,6 +52,12 @@ public final class Controller { .rightTriggerDeadZone(0.1f); ButtonState buttonState = ButtonState.fromController(this); state = new ControllerState(axesState, buttonState); + + ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(this); + } + + public ControllerBindings bindings() { + return bindings; } public boolean connected() { @@ -89,7 +100,7 @@ public final class Controller { @Override public int hashCode() { - return Objects.hash(id); + return Objects.hash(guid); } @Override diff --git a/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java new file mode 100644 index 0000000..5fa27f4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java @@ -0,0 +1,30 @@ +package dev.isxander.controlify.event; + +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.controller.Controller; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public class ControlifyEvents { + public static final Event INPUT_MODE_CHANGED = EventFactory.createArrayBacked(InputModeChanged.class, callbacks -> mode -> { + for (InputModeChanged callback : callbacks) { + callback.onInputModeChanged(mode); + } + }); + + public static final Event CONTROLLER_STATE_UPDATED = EventFactory.createArrayBacked(ControllerStateUpdate.class, callbacks -> controller -> { + for (ControllerStateUpdate callback : callbacks) { + callback.onControllerStateUpdate(controller); + } + }); + + @FunctionalInterface + public interface InputModeChanged { + void onInputModeChanged(InputMode mode); + } + + @FunctionalInterface + public interface ControllerStateUpdate { + void onControllerStateUpdate(Controller controller); + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java new file mode 100644 index 0000000..a902662 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java @@ -0,0 +1,36 @@ +package dev.isxander.controlify.ingame; + +import dev.isxander.controlify.controller.Controller; +import net.minecraft.client.player.Input; + +public class ControllerPlayerMovement extends Input { + private final Controller controller; + + public ControllerPlayerMovement(Controller controller) { + this.controller = controller; + } + + @Override + public void tick(boolean slowDown, float f) { + var axes = controller.state().axes(); + + this.up = axes.leftStickY() < 0; + this.down = axes.leftStickY() > 0; + this.left = axes.leftStickX() < 0; + this.right = axes.leftStickX() > 0; + this.leftImpulse = -axes.leftStickX(); + this.forwardImpulse = -axes.leftStickY(); + + if (slowDown) { + this.leftImpulse *= f; + this.forwardImpulse *= f; + } + + var bindings = controller.bindings(); + + this.jumping = bindings.JUMP.held(); + if (bindings.SNEAK.justPressed()) { + this.shiftKeyDown = !this.shiftKeyDown; + } + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java new file mode 100644 index 0000000..aad1da3 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java @@ -0,0 +1,56 @@ +package dev.isxander.controlify.ingame; + +import com.mojang.blaze3d.Blaze3D; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.event.ControlifyEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.KeyboardInput; + +public class InGameInputHandler { + private final Controller controller; + private final Minecraft minecraft; + + private double accumulatedDX, accumulatedDY; + private double deltaTime; + + public InGameInputHandler(Controller controller) { + this.controller = controller; + this.minecraft = Minecraft.getInstance(); + + ControlifyEvents.INPUT_MODE_CHANGED.register(mode -> { + if (minecraft.player != null) { + minecraft.player.input = mode == InputMode.CONTROLLER + ? new ControllerPlayerMovement(controller) + : new KeyboardInput(minecraft.options); + } + }); + } + + public void inputTick() { + var axes = controller.state().axes(); + if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive()) { + accumulatedDX += axes.rightStickX(); + accumulatedDY += axes.rightStickY(); + } + + processPlayerLook(); + } + + public void processPlayerLook() { + var time = Blaze3D.getTime(); + var delta = time - deltaTime; + deltaTime = time; + + var sensitivity = 1f * 8f + 2f; + var sensCubed = sensitivity * sensitivity * sensitivity; + + var dx = accumulatedDX * delta; + var dy = accumulatedDY * delta; + accumulatedDX -= dx * 20; // 20 is how quickly the camera will slow down + accumulatedDY -= dy * 20; + + if (minecraft.player != null) + minecraft.player.turn(dx * sensCubed, dy * sensCubed); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java index 4a72288..79d5ee3 100644 --- a/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java @@ -8,6 +8,9 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +/** + * Mixin to insert a custom {@link ComponentProcessor} into slider to support left/right movement without navigating to next component. + */ @Mixin(AbstractSliderButton.class) public class AbstractSliderButtonMixin implements ComponentProcessorProvider { @Shadow private boolean canChangeValue; diff --git a/src/main/java/dev/isxander/controlify/mixins/ClientPacketListenerMixin.java b/src/main/java/dev/isxander/controlify/mixins/ClientPacketListenerMixin.java new file mode 100644 index 0000000..c8ec1c8 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/ClientPacketListenerMixin.java @@ -0,0 +1,35 @@ +package dev.isxander.controlify.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.ingame.ControllerPlayerMovement; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.player.Input; +import net.minecraft.client.player.KeyboardInput; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Override input handling for main player. + */ +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin { + @Shadow @Final private Minecraft minecraft; + + @Inject(method = "handleLogin", at = @At(value = "FIELD", target = "Lnet/minecraft/client/player/LocalPlayer;input:Lnet/minecraft/client/player/Input;", opcode = Opcodes.ASTORE, shift = At.Shift.AFTER)) + private void useControllerInput(ClientboundLoginPacket packet, CallbackInfo ci) { + if (Controlify.getInstance().getCurrentInputMode() == InputMode.CONTROLLER && minecraft.player != null) + minecraft.player.input = new ControllerPlayerMovement(Controlify.getInstance().getCurrentController()); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java index 564c456..b724e23 100644 --- a/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java @@ -13,4 +13,9 @@ public class MinecraftMixin { private void onInputInitialized(CallbackInfo ci) { Controlify.getInstance().onInitializeInput(); } + + @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;turnPlayer()V")) + private void doPlayerLook(boolean tick, CallbackInfo ci) { + Controlify.getInstance().getInGameInputHandler().processPlayerLook(); + } } diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index b405ddc..d3ac661 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -7,6 +7,7 @@ ], "client": [ "AbstractSliderButtonMixin", + "ClientPacketListenerMixin", "KeyboardHandlerMixin", "MinecraftMixin", "MouseHandlerMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index af3c6f6..d104f32 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -15,7 +15,9 @@ "license": "LGPL-3.0-or-later", "environment": "client", "entrypoints": { - + "preLaunch": [ + "com.llamalad7.mixinextras.MixinExtrasBootstrap::init" + ] }, "mixins": [ "controlify.mixins.json"