From 1ba701d2cd1aa53ea4ace4f671400a3e428ad6b6 Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 31 Jan 2023 18:39:30 +0000 Subject: [PATCH] controllers! --- build.gradle.kts | 34 ++--- gradle.properties | 8 +- gradle/libs.versions.toml | 12 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 +- .../dev/isxander/controlify/Controlify.java | 74 +++++++++++ .../dev/isxander/controlify/InputMode.java | 6 + .../compatibility/screen/ScreenProcessor.java | 100 +++++++++++++++ .../screen/ScreenProcessorProvider.java | 11 ++ .../screen/component/ComponentProcessor.java | 26 ++++ .../component/ComponentProcessorProvider.java | 13 ++ .../component/SliderComponentProcessor.java | 65 ++++++++++ .../controlify/controller/AxesState.java | 61 +++++++++ .../controlify/controller/ButtonState.java | 44 +++++++ .../controlify/controller/Controller.java | 119 ++++++++++++++++++ .../controller/ControllerState.java | 9 ++ .../mixins/AbstractSliderButtonMixin.java | 26 ++++ .../mixins/KeyboardHandlerMixin.java | 17 +++ .../controlify/mixins/MinecraftMixin.java | 16 +++ .../controlify/mixins/MouseHandlerMixin.java | 27 ++++ .../controlify/mixins/ScreenAccessor.java | 17 +++ .../controlify/mixins/ScreenMixin.java | 26 ++++ .../dev/isxander/fabricmodtemplate/.gitkeep | 0 .../fabricmodtemplate/mixins/.gitkeep | 0 src/main/resources/controlify.mixins.json | 16 +++ .../resources/fabric-mod-template.mixins.json | 9 -- src/main/resources/fabric.mod.json | 2 +- 27 files changed, 693 insertions(+), 49 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/Controlify.java create mode 100644 src/main/java/dev/isxander/controlify/InputMode.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessorProvider.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java create mode 100644 src/main/java/dev/isxander/controlify/controller/AxesState.java create mode 100644 src/main/java/dev/isxander/controlify/controller/ButtonState.java create mode 100644 src/main/java/dev/isxander/controlify/controller/Controller.java create mode 100644 src/main/java/dev/isxander/controlify/controller/ControllerState.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/KeyboardHandlerMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/MouseHandlerMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/ScreenAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/ScreenMixin.java delete mode 100644 src/main/java/dev/isxander/fabricmodtemplate/.gitkeep delete mode 100644 src/main/java/dev/isxander/fabricmodtemplate/mixins/.gitkeep create mode 100644 src/main/resources/controlify.mixins.json delete mode 100644 src/main/resources/fabric-mod-template.mixins.json diff --git a/build.gradle.kts b/build.gradle.kts index dbe06f9..fe25167 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,31 +15,11 @@ plugins { group = "dev.isxander" version = "1.0.0+1.19.3" -/* UNCOMMENT OR DELETE IF YOU WANT TESTMOD SOURCESET -val testmod by sourceSets.registering { - compileClasspath += sourceSets.main.get().compileClasspath - runtimeClasspath += sourceSets.main.get().runtimeClasspath -} - -loom { - runs { - register("testmod") { - client() - ideConfigGenerated(true) - name("Test Mod") - source(testmod.get()) - } - } - - createRemapConfigurations(testmod.get()) -} -*/ - repositories { mavenCentral() maven("https://maven.terraformersmc.com") maven("https://maven.isxander.dev/releases") - + maven("https://maven.quiltmc.org/repository/release") maven("https://api.modrinth.com/maven") { name = "Modrinth" content { @@ -52,13 +32,13 @@ val minecraftVersion = libs.versions.minecraft.get() dependencies { minecraft(libs.minecraft) - mappings("net.fabricmc:yarn:$minecraftVersion+build.${libs.versions.yarn.get()}:v2") + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) modImplementation(libs.fabric.loader) -// modImplementation(libs.fabric.api) -// modImplementation(fabricApi.module("fabric-resource-loader-v0", libs.versions.fabric.api.get())) - - modRuntimeOnly("maven.modrinth:smoothboot-fabric:1.19-1.7.1") // improve system performance when booting dev env + modImplementation(libs.fabric.api) } tasks { @@ -180,7 +160,7 @@ publishing { val password = "XANDER_MAVEN_PASS".let { System.getenv(it) ?: findProperty(it) }?.toString() if (username != null && password != null) { maven(url = "https://maven.isxander.dev/releases") { - name = "Xander Releases" + name = "XanderReleases" credentials { this.username = username this.password = password diff --git a/gradle.properties b/gradle.properties index 09bad7f..6257670 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ org.gradle.jvmargs=-Xmx4G -modId=fabric-mod-template -modName=Fabric Mod Template -modDescription=Template for Xander's mods with extra gradle goodies. +modId=controlify +modName=Controlify +modDescription=Another controller mod - for fabric! curseforgeId= modrinthId= -githubProject=isXander/FabricModTemplate +githubProject=isXander/Controlify diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7129ab5..38b5847 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -loom = "1.0.+" +loom = "1.1.+" loom_quiltflower = "1.8.+" -minotaur = "2.5.+" +minotaur = "2.6.+" cursegradle = "2.+" github_release = "2.+" machete = "1.+" grgit = "5.0.+" -minecraft = "1.19.3" -yarn = "4" -fabric_loader = "0.14.12" -fabric_api = "0.69.1+1.19.3" +minecraft = "23w04a" +quilt_mappings = "10" +fabric_loader = "0.14.13" +fabric_api = "0.73.1+1.19.4" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92f06b5..f42e62f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 9349f25..c1f6aa2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,5 +13,5 @@ dependencyResolutionManagement { } } -rootProject.name = "FabricModTemplate" +rootProject.name = "Controlify" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java new file mode 100644 index 0000000..afdab1e --- /dev/null +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -0,0 +1,74 @@ +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 net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.Minecraft; +import org.lwjgl.glfw.GLFW; + +public class Controlify { + private static Controlify instance = null; + + private Controller currentController; + 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); + System.out.println("Connected: " + currentController.name()); + this.setCurrentInputMode(InputMode.CONTROLLER); + } + } + + // listen for new controllers + GLFW.glfwSetJoystickCallback((jid, event) -> { + System.out.println("Event: " + event); + if (event == GLFW.GLFW_CONNECTED) { + currentController = 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); + System.out.println("Disconnected: " + jid); + this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); + } + }); + + ClientTickEvents.START_CLIENT_TICK.register(client -> { + updateControllers(); + }); + } + + public void updateControllers() { + for (Controller controller : Controller.CONTROLLERS.values()) { + controller.updateState(); + } + + ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state(); + + if (state.hasAnyInput()) + this.setCurrentInputMode(InputMode.CONTROLLER); + + Minecraft client = Minecraft.getInstance(); + if (client.screen != null && currentController != null) ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + } + + public InputMode getCurrentInputMode() { + return currentInputMode; + } + + public void setCurrentInputMode(InputMode currentInputMode) { + this.currentInputMode = currentInputMode; + } + + public static Controlify getInstance() { + if (instance == null) instance = new Controlify(); + return instance; + } +} diff --git a/src/main/java/dev/isxander/controlify/InputMode.java b/src/main/java/dev/isxander/controlify/InputMode.java new file mode 100644 index 0000000..0bee16c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/InputMode.java @@ -0,0 +1,6 @@ +package dev.isxander.controlify; + +public enum InputMode { + KEYBOARD_MOUSE, + CONTROLLER; +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java new file mode 100644 index 0000000..421d52d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java @@ -0,0 +1,100 @@ +package dev.isxander.controlify.compatibility.screen; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.mixins.ScreenAccessor; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.screens.Screen; +import org.lwjgl.glfw.GLFW; + +public class ScreenProcessor { + private static final int REPEAT_DELAY = 5; + private static final int INITIAL_REPEAT_DELAY = 20; + + public final Screen screen; + private int lastMoved = 0; + + public ScreenProcessor(Screen screen) { + this.screen = screen; + } + + public void onControllerUpdate(Controller controller) { + handleComponentNavigation(controller); + handleButtons(controller); + } + + private void handleComponentNavigation(Controller controller) { + if (screen.getFocused() != null) { + var focused = screen.getFocused(); + var processor = ComponentProcessorProvider.provide(focused); + if (processor.overrideControllerNavigation(this, controller)) return; + } + + var accessor = (ScreenAccessor) screen; + + boolean repeatEventAvailable = ++lastMoved > INITIAL_REPEAT_DELAY; + + var axes = controller.state().axes(); + var prevAxes = controller.prevState().axes(); + var buttons = controller.state().buttons(); + var prevButtons = controller.prevState().buttons(); + + FocusNavigationEvent.ArrowNavigation event = null; + if (axes.leftStickX() > 0.5f && (repeatEventAvailable || prevAxes.leftStickX() <= 0.5f)) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT); + } else if (axes.leftStickX() < -0.5f && (repeatEventAvailable || prevAxes.leftStickX() >= -0.5f)) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT); + } else if (axes.leftStickY() < -0.5f && (repeatEventAvailable || prevAxes.leftStickY() >= -0.5f)) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.UP); + } else if (axes.leftStickY() > 0.5f && (repeatEventAvailable || prevAxes.leftStickY() <= 0.5f)) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN); + } else if (buttons.dpadUp() && (repeatEventAvailable || !prevButtons.dpadUp())) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.UP); + } else if (buttons.dpadDown() && (repeatEventAvailable || !prevButtons.dpadDown())) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN); + } else if (buttons.dpadLeft() && (repeatEventAvailable || !prevButtons.dpadLeft())) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT); + } else if (buttons.dpadRight() && (repeatEventAvailable || !prevButtons.dpadRight())) { + event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT); + } + + if (event != null) { + ComponentPath path = screen.nextFocusPath(event); + if (path != null) { + accessor.invokeChangeFocus(path); + lastMoved = repeatEventAvailable ? INITIAL_REPEAT_DELAY - REPEAT_DELAY : 0; + } + } + } + + private void handleButtons(Controller controller) { + if (screen.getFocused() != null) { + var focused = screen.getFocused(); + var processor = ComponentProcessorProvider.provide(focused); + if (processor.overrideControllerButtons(this, controller)) return; + } + + var buttons = controller.state().buttons(); + var prevButtons = controller.prevState().buttons(); + + if (buttons.a() && !prevButtons.a()) + screen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0); + if (buttons.b() && !prevButtons.b()) + screen.onClose(); + } + + public void onWidgetRebuild() { + // initial focus + if (screen.getFocused() == null && Controlify.getInstance().getCurrentInputMode() == InputMode.CONTROLLER) { + var accessor = (ScreenAccessor) screen; + ComponentPath path = screen.nextFocusPath(accessor.invokeCreateArrowEvent(ScreenDirection.DOWN)); + if (path != null) + accessor.invokeChangeFocus(path); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java new file mode 100644 index 0000000..0625c33 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java @@ -0,0 +1,11 @@ +package dev.isxander.controlify.compatibility.screen; + +import net.minecraft.client.gui.screens.Screen; + +public interface ScreenProcessorProvider { + ScreenProcessor screenProcessor(); + + static ScreenProcessor provide(Screen screen) { + return ((ScreenProcessorProvider) screen).screenProcessor(); + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java new file mode 100644 index 0000000..ee32ba4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java @@ -0,0 +1,26 @@ +package dev.isxander.controlify.compatibility.screen.component; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +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 net.minecraft.client.gui.components.events.GuiEventListener; + +public class ComponentProcessor { + static final ComponentProcessor EMPTY = new ComponentProcessor<>(null); + + protected final T component; + + public ComponentProcessor(T component) { + this.component = component; + } + + public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { + return false; + } + + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessorProvider.java b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessorProvider.java new file mode 100644 index 0000000..55c9feb --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessorProvider.java @@ -0,0 +1,13 @@ +package dev.isxander.controlify.compatibility.screen.component; + +import net.minecraft.client.gui.components.events.GuiEventListener; + +public interface ComponentProcessorProvider { + ComponentProcessor componentProcessor(); + + static ComponentProcessor provide(GuiEventListener component) { + if (component instanceof ComponentProcessorProvider provider) + return provider.componentProcessor(); + return ComponentProcessor.EMPTY; + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java new file mode 100644 index 0000000..4df1352 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java @@ -0,0 +1,65 @@ +package dev.isxander.controlify.compatibility.screen.component; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerState; +import net.minecraft.client.gui.components.AbstractSliderButton; +import org.lwjgl.glfw.GLFW; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SliderComponentProcessor extends ComponentProcessor { + private final Supplier canChangeValueGetter; + private final Consumer canChangeValueSetter; + + private static final int SLIDER_CHANGE_DELAY = 1; + private int lastSliderChange = SLIDER_CHANGE_DELAY; + + public SliderComponentProcessor(AbstractSliderButton component, Supplier canChangeValueGetter, Consumer canChangeValueSetter) { + super(component); + this.canChangeValueGetter = canChangeValueGetter; + this.canChangeValueSetter = canChangeValueSetter; + } + + @Override + public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { + if (!this.canChangeValueGetter.get()) return false; + + var canSliderChange = ++lastSliderChange > SLIDER_CHANGE_DELAY; + + var axes = controller.state().axes(); + var buttons = controller.state().buttons(); + if (axes.leftStickX() > 0.5f || buttons.dpadRight()) { + if (canSliderChange) { + component.keyPressed(GLFW.GLFW_KEY_RIGHT, 0, 0); + lastSliderChange = 0; + } + + return true; + } else if (axes.leftStickX() < -0.5f || buttons.dpadLeft()) { + if (canSliderChange) { + component.keyPressed(GLFW.GLFW_KEY_LEFT, 0, 0); + lastSliderChange = 0; + } + + return true; + } + + return false; + } + + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (!this.canChangeValueGetter.get()) return false; + + var buttons = controller.state().buttons(); + var prevButtons = controller.prevState().buttons(); + if (buttons.b() && !prevButtons.b()) { + this.canChangeValueSetter.accept(false); + return true; + } + + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/AxesState.java b/src/main/java/dev/isxander/controlify/controller/AxesState.java new file mode 100644 index 0000000..715938d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/AxesState.java @@ -0,0 +1,61 @@ +package dev.isxander.controlify.controller; + +import org.lwjgl.glfw.GLFW; + +public record AxesState( + float leftStickX, float leftStickY, + float rightStickX, float rightStickY, + float leftTrigger, float rightTrigger +) { + public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0); + + public AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) { + return new AxesState( + Math.abs(leftStickX) < deadZoneX ? 0 : leftStickX, + Math.abs(leftStickY) < deadZoneY ? 0 : leftStickY, + rightStickX, rightStickY, leftTrigger, rightTrigger + ); + } + + public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) { + return new AxesState( + leftStickX, leftStickY, + Math.abs(rightStickX) < deadZoneX ? 0 : rightStickX, + Math.abs(rightStickY) < deadZoneY ? 0 : rightStickY, + leftTrigger, rightTrigger + ); + } + + public AxesState leftTriggerDeadZone(float deadZone) { + return new AxesState( + leftStickX, leftStickY, rightStickX, rightStickY, + Math.abs(leftTrigger) < deadZone ? 0 : leftTrigger, + rightTrigger + ); + } + + public AxesState rightTriggerDeadZone(float deadZone) { + return new AxesState( + leftStickX, leftStickY, rightStickX, rightStickY, + leftTrigger, + Math.abs(rightTrigger) < deadZone ? 0 : rightTrigger + ); + } + + public static AxesState fromController(Controller controller) { + if (controller == null || !controller.connected()) + return EMPTY; + + var state = controller.getGamepadState(); + var axes = state.axes(); + + float leftX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X); + float leftY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y); + float rightX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X); + float rightY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y); + float leftTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER) + 1f) / 2f; + float rightTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) + 1f) / 2f; + + return new AxesState(leftX, leftY, rightX, rightY, leftTrigger, rightTrigger); + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/ButtonState.java b/src/main/java/dev/isxander/controlify/controller/ButtonState.java new file mode 100644 index 0000000..e2e4a8f --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/ButtonState.java @@ -0,0 +1,44 @@ +package dev.isxander.controlify.controller; + +import org.lwjgl.glfw.GLFW; + +public record ButtonState( + boolean a, boolean b, boolean x, boolean y, + boolean leftBumper, boolean rightBumper, + boolean back, boolean start, + boolean dpadUp, boolean dpadDown, boolean dpadLeft, boolean dpadRight, + boolean leftStick, boolean rightStick +) { + public static ButtonState EMPTY = new ButtonState( + false, false, false, false, + false, false, + false, false, + false, false, false, false, + false, false + ); + + public static ButtonState fromController(Controller controller) { + if (controller == null || !controller.connected()) + return EMPTY; + + var state = controller.getGamepadState(); + var buttons = state.buttons(); + + boolean a = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_A) == GLFW.GLFW_PRESS; + boolean b = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_B) == GLFW.GLFW_PRESS; + boolean x = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_X) == GLFW.GLFW_PRESS; + boolean y = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_Y) == GLFW.GLFW_PRESS; + boolean leftBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == GLFW.GLFW_PRESS; + boolean rightBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == GLFW.GLFW_PRESS; + boolean back = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_BACK) == GLFW.GLFW_PRESS; + boolean start = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_START) == GLFW.GLFW_PRESS; + boolean dpadUp = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW.GLFW_PRESS; + boolean dpadDown = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW.GLFW_PRESS; + boolean dpadLeft = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW.GLFW_PRESS; + boolean dpadRight = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW.GLFW_PRESS; + boolean leftStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_THUMB) == GLFW.GLFW_PRESS; + boolean rightStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB) == GLFW.GLFW_PRESS; + + return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick); + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java new file mode 100644 index 0000000..8f7faf8 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -0,0 +1,119 @@ +package dev.isxander.controlify.controller; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWGamepadState; + +import java.util.HashMap; +import java.util.Map; +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; + + public Controller(int id, String guid, String name, boolean gamepad) { + this.id = id; + this.guid = guid; + this.name = name; + this.gamepad = gamepad; + } + + public ControllerState state() { + return state; + } + + public ControllerState prevState() { + return prevState; + } + + public void updateState() { + if (!connected()) { + state = prevState = ControllerState.EMPTY; + return; + } + + prevState = state; + + AxesState axesState = AxesState.fromController(this) + .leftJoystickDeadZone(0.2f, 0.2f) + .rightJoystickDeadZone(0.2f, 0.2f) + .leftTriggerDeadZone(0.1f) + .rightTriggerDeadZone(0.1f); + ButtonState buttonState = ButtonState.fromController(this); + state = new ControllerState(axesState, buttonState); + } + + public boolean connected() { + return GLFW.glfwJoystickPresent(id); + } + + GLFWGamepadState getGamepadState() { + GLFWGamepadState state = GLFWGamepadState.create(); + if (gamepad) + GLFW.glfwGetGamepadState(id, state); + return state; + } + + public int id() { + return id; + } + + public String guid() { + return guid; + } + + public String name() { + return name; + } + + public boolean gamepad() { + return gamepad; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Controller) obj; + return this.id == that.id && + Objects.equals(this.guid, that.guid) && + Objects.equals(this.name, that.name) && + this.gamepad == that.gamepad; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Controller[" + + "id=" + id + ", " + + "name=" + name + ']'; + } + + public static Controller byId(int id) { + if (id > GLFW.GLFW_JOYSTICK_LAST) + throw new IllegalArgumentException("Invalid joystick id: " + id); + if (CONTROLLERS.containsKey(id)) + return CONTROLLERS.get(id); + + String guid = GLFW.glfwGetJoystickGUID(id); + boolean gamepad = GLFW.glfwJoystickIsGamepad(id); + String name = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id); + if (name == null) name = Integer.toString(id); + + Controller controller = new Controller(id, guid, name, gamepad); + CONTROLLERS.put(id, controller); + + return controller; + } + +} diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerState.java b/src/main/java/dev/isxander/controlify/controller/ControllerState.java new file mode 100644 index 0000000..5e12b09 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/ControllerState.java @@ -0,0 +1,9 @@ +package dev.isxander.controlify.controller; + +public record ControllerState(AxesState axes, ButtonState buttons) { + public static final ControllerState EMPTY = new ControllerState(AxesState.EMPTY, ButtonState.EMPTY); + + public boolean hasAnyInput() { + return !this.equals(EMPTY); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java new file mode 100644 index 0000000..4a72288 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/AbstractSliderButtonMixin.java @@ -0,0 +1,26 @@ +package dev.isxander.controlify.mixins; + +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; +import dev.isxander.controlify.compatibility.screen.component.SliderComponentProcessor; +import net.minecraft.client.gui.components.AbstractSliderButton; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(AbstractSliderButton.class) +public class AbstractSliderButtonMixin implements ComponentProcessorProvider { + @Shadow private boolean canChangeValue; + + @Unique + private final SliderComponentProcessor controlify$processor = new SliderComponentProcessor( + (AbstractSliderButton) (Object) this, + () -> this.canChangeValue, + val -> this.canChangeValue = val + ); + + @Override + public ComponentProcessor componentProcessor() { + return controlify$processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/KeyboardHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/KeyboardHandlerMixin.java new file mode 100644 index 0000000..737d8b6 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/KeyboardHandlerMixin.java @@ -0,0 +1,17 @@ +package dev.isxander.controlify.mixins; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import net.minecraft.client.KeyboardHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(KeyboardHandler.class) +public class KeyboardHandlerMixin { + @Inject(method = "keyPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setLastInputType(Lnet/minecraft/client/InputType;)V")) + private void onKeyboardInput(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) { + Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java new file mode 100644 index 0000000..564c456 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/MinecraftMixin.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.mixins; + +import dev.isxander.controlify.Controlify; +import net.minecraft.client.Minecraft; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;setup(J)V", shift = At.Shift.AFTER)) + private void onInputInitialized(CallbackInfo ci) { + Controlify.getInstance().onInitializeInput(); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/MouseHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/MouseHandlerMixin.java new file mode 100644 index 0000000..14f023f --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/MouseHandlerMixin.java @@ -0,0 +1,27 @@ +package dev.isxander.controlify.mixins; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import net.minecraft.client.MouseHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MouseHandler.class) +public class MouseHandlerMixin { + @Inject(method = "onPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setLastInputType(Lnet/minecraft/client/InputType;)V")) + private void onMouseClickInput(long window, int button, int action, int modifiers, CallbackInfo ci) { + Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + } + + @Inject(method = "onMove", at = @At("RETURN")) + private void onMouseMoveInput(long window, double x, double y, CallbackInfo ci) { + Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + } + + @Inject(method = "onScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getOverlay()Lnet/minecraft/client/gui/screens/Overlay;")) + private void onMouseScrollInput(long window, double scrollDeltaX, double scrollDeltaY, CallbackInfo ci) { + Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/ScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/ScreenAccessor.java new file mode 100644 index 0000000..8ea82af --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/ScreenAccessor.java @@ -0,0 +1,17 @@ +package dev.isxander.controlify.mixins; + +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Screen.class) +public interface ScreenAccessor { + @Invoker + FocusNavigationEvent.ArrowNavigation invokeCreateArrowEvent(ScreenDirection direction); + + @Invoker + void invokeChangeFocus(ComponentPath path); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/ScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/ScreenMixin.java new file mode 100644 index 0000000..bfec972 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/ScreenMixin.java @@ -0,0 +1,26 @@ +package dev.isxander.controlify.mixins; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider; +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Screen.class) +public class ScreenMixin implements ScreenProcessorProvider { + @Unique + private final ScreenProcessor controlify$processor = new ScreenProcessor((Screen) (Object) this); + + @Override + public ScreenProcessor screenProcessor() { + return controlify$processor; + } + + @Inject(method = "rebuildWidgets", at = @At("RETURN")) + private void onScreenInit(CallbackInfo ci) { + screenProcessor().onWidgetRebuild(); + } +} diff --git a/src/main/java/dev/isxander/fabricmodtemplate/.gitkeep b/src/main/java/dev/isxander/fabricmodtemplate/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/dev/isxander/fabricmodtemplate/mixins/.gitkeep b/src/main/java/dev/isxander/fabricmodtemplate/mixins/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json new file mode 100644 index 0000000..b405ddc --- /dev/null +++ b/src/main/resources/controlify.mixins.json @@ -0,0 +1,16 @@ +{ + "package": "dev.isxander.controlify.mixins", + "required": true, + "minVersion": "0.8", + "compatibilityLevel": "JAVA_17", + "mixins": [ + ], + "client": [ + "AbstractSliderButtonMixin", + "KeyboardHandlerMixin", + "MinecraftMixin", + "MouseHandlerMixin", + "ScreenAccessor", + "ScreenMixin" + ] +} diff --git a/src/main/resources/fabric-mod-template.mixins.json b/src/main/resources/fabric-mod-template.mixins.json deleted file mode 100644 index 9ac6462..0000000 --- a/src/main/resources/fabric-mod-template.mixins.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "package": "dev.isxander.fabricmodtemplate.mixins", - "required": true, - "minVersion": "0.8", - "compatibilityLevel": "JAVA_17", - "mixins": [ - - ] -} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 95b5d16..af3c6f6 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -18,7 +18,7 @@ }, "mixins": [ - "fabric-mod-template.mixins.json" + "controlify.mixins.json" ], "depends": { "fabricloader": ">=0.14.0",