From d3fc0a946b9d51e234ae201c0fe0428844176578 Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 11 Apr 2023 11:03:07 +0100 Subject: [PATCH] rewrite most of joystick mapping --- .../dev/isxander/controlify/Controlify.java | 87 +++-- .../bindings/ControllerBindings.java | 11 + .../controlify/bindings/JoystickAxisBind.java | 4 +- .../bindings/JoystickButtonBind.java | 2 +- .../controlify/bindings/JoystickHatBind.java | 8 +- .../controlify/controller/ControllerType.java | 10 +- .../joystick/CompoundJoystickController.java | 2 +- .../joystick/CompoundJoystickInfo.java | 2 +- .../controller/joystick/JoystickConfig.java | 13 +- .../joystick/JoystickController.java | 3 + .../controller/joystick/JoystickState.java | 102 +++--- .../joystick/SingleJoystickController.java | 13 +- .../joystick/mapping/JoystickMapping.java | 18 +- .../joystick/mapping/RPJoystickMapping.java | 340 +++++++++++++----- .../mapping/UnmappedJoystickMapping.java | 69 +++- .../controlify/debug/DebugProperties.java | 19 +- .../blockbreak/MultiPlayerGameModeMixin.java | 2 + .../controlify/rumble/BasicRumbleEffect.java | 45 ++- src/main/resources/fabric.mod.json | 2 +- 19 files changed, 535 insertions(+), 217 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 000e965..41f85d4 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -21,6 +21,9 @@ import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; @@ -78,12 +81,14 @@ public class Controlify implements ControlifyApi { if (config().globalSettings().loadVibrationNatives) SDL2NativesManager.initialise(); - boolean dirtyControllerConfig = false; // find already connected controllers for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) { if (GLFW.glfwJoystickPresent(jid)) { try { - var controller = Controller.createOrGet(jid, controllerHIDService.fetchType()); + var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType()); + if (controllerOpt.isEmpty()) continue; + var controller = controllerOpt.get(); + LOGGER.info("Controller found: " + controller.name()); config().loadOrCreateControllerData(controller); @@ -93,7 +98,7 @@ public class Controlify implements ControlifyApi { if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { controller.config().allowVibrations = false; - dirtyControllerConfig = true; + config().setDirty(); } } catch (Exception e) { LOGGER.error("Failed to initialize controller with jid " + jid, e); @@ -101,10 +106,6 @@ public class Controlify implements ControlifyApi { } } - if (dirtyControllerConfig) { - config().save(); - } - checkCompoundJoysticks(); if (Controller.CONTROLLERS.isEmpty()) { @@ -113,6 +114,9 @@ public class Controlify implements ControlifyApi { if (currentController() == Controller.DUMMY && config().isFirstLaunch()) { this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); + } else { + // setCurrentController saves config + config().saveIfDirty(); } // listen for new controllers @@ -172,16 +176,11 @@ public class Controlify implements ControlifyApi { for (var controller : Controller.CONTROLLERS.values()) { if (!outOfFocus) - controller.updateState(); - else { - controller.clearState(); - controller.rumbleManager().clearEffects(); - } - controller.rumbleManager().tick(); + wrapControllerError(controller::updateState, "Updating controller state", controller); + else + wrapControllerError(controller::clearState, "Clearing controller state", controller); } - ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state(); - if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) { if (switchableController.state().hasAnyInput()) { this.setCurrentController(switchableController); @@ -189,13 +188,23 @@ public class Controlify implements ControlifyApi { askSwitchToast.remove(); askSwitchToast = null; } + switchableController.clearState(); switchableController = null; - state = ControllerState.EMPTY; } } - if (outOfFocus) + wrapControllerError(() -> tickController(currentController, outOfFocus), "Ticking current controller", currentController); + } + + private void tickController(Controller controller, boolean outOfFocus) { + ControllerState state = controller.state(); + + if (outOfFocus) { state = ControllerState.EMPTY; + controller.rumbleManager().clearEffects(); + } else { + controller.rumbleManager().tick(); + } if (state.hasAnyInput()) this.setInputMode(InputMode.CONTROLLER); @@ -209,22 +218,31 @@ public class Controlify implements ControlifyApi { ); this.setCurrentController(null); consecutiveInputSwitches = 0; - } - - if (currentController == null) { - this.setInputMode(InputMode.KEYBOARD_MOUSE); return; } - if (client.screen != null) { - ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + if (minecraft.screen != null) { + ScreenProcessorProvider.provide(minecraft.screen).onControllerUpdate(controller); } - if (client.level != null) { + if (minecraft.level != null) { this.inGameInputHandler().inputTick(); } - this.virtualMouseHandler().handleControllerInput(currentController); + this.virtualMouseHandler().handleControllerInput(controller); - ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(currentController); + ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(controller); + } + + public static void wrapControllerError(Runnable runnable, String errorTitle, Controller controller) { + try { + runnable.run(); + } catch (Throwable e) { + CrashReport crashReport = CrashReport.forThrowable(e, errorTitle); + CrashReportCategory category = crashReport.addCategory("Affected controller"); + category.setDetail("Controller name", controller::name); + category.setDetail("Controller identification", () -> controller.type().toString()); + category.setDetail("Controller type", () -> controller.getClass().getCanonicalName()); + throw new ReportedException(crashReport); + } } public ControlifyConfig config() { @@ -232,14 +250,24 @@ public class Controlify implements ControlifyApi { } private void onControllerHotplugged(int jid) { - var controller = Controller.createOrGet(jid, controllerHIDService.fetchType()); + var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType()); + if (controllerOpt.isEmpty()) return; + var controller = controllerOpt.get(); + LOGGER.info("Controller connected: " + controller.name()); config().loadOrCreateControllerData(currentController); + if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { + controller.config().allowVibrations = false; + config().setDirty(); + } + this.askToSwitchController(controller); checkCompoundJoysticks(); + + config().saveIfDirty(); } private void onControllerDisconnect(int jid) { @@ -304,7 +332,12 @@ public class Controlify implements ControlifyApi { controller = Controller.DUMMY; if (this.currentController == controller) return; + + if (this.currentController != null) + this.currentController.close(); + this.currentController = controller; + this.currentController.open(); if (switchableController == controller) { switchableController = null; diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index cac05d4..fa1e8d5 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -8,6 +8,7 @@ import dev.isxander.controlify.api.bind.ControllerBindingBuilder; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.api.event.ControlifyEvents; +import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.mixins.compat.fapi.KeyBindingRegistryImplAccessor; import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor; import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor; @@ -38,6 +39,7 @@ public class ControllerBindings { public final ControllerBinding WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT, LOOK_UP, LOOK_DOWN, LOOK_LEFT, LOOK_RIGHT, + GAMEPAD_GYRO_BUTTON, JUMP, SNEAK, ATTACK, USE, SPRINT, @@ -110,6 +112,15 @@ public class ControllerBindings { .defaultBind(GamepadBinds.RIGHT_STICK_RIGHT) .category(MOVEMENT_CATEGORY) .build()); + if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) { + register(GAMEPAD_GYRO_BUTTON = ControllerBindingBuilder.create(controller) + .identifier("controlify", "gamepad_gyro_button") + .defaultBind(new EmptyBind<>()) + .category(MOVEMENT_CATEGORY) + .build()); + } else { + GAMEPAD_GYRO_BUTTON = null; + } register(JUMP = ControllerBindingBuilder.create(controller) .identifier("controlify", "jump") .defaultBind(GamepadBinds.A_BUTTON) diff --git a/src/main/java/dev/isxander/controlify/bindings/JoystickAxisBind.java b/src/main/java/dev/isxander/controlify/bindings/JoystickAxisBind.java index 1cc463b..1fa638e 100644 --- a/src/main/java/dev/isxander/controlify/bindings/JoystickAxisBind.java +++ b/src/main/java/dev/isxander/controlify/bindings/JoystickAxisBind.java @@ -42,8 +42,8 @@ public class JoystickAxisBind implements IBind { JoystickMapping mapping = joystick.mapping(); String type = joystick.type().identifier(); - String axis = mapping.axis(axisIndex).identifier(); - String direction = mapping.axis(axisIndex).getDirectionIdentifier(axisIndex, this.direction); + String axis = mapping.axes()[axisIndex].identifier(); + String direction = mapping.axes()[axisIndex].getDirectionIdentifier(axisIndex, this.direction); var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/axis_" + axis + "_" + direction + ".png"); RenderSystem.setShaderTexture(0, texture); diff --git a/src/main/java/dev/isxander/controlify/bindings/JoystickButtonBind.java b/src/main/java/dev/isxander/controlify/bindings/JoystickButtonBind.java index ee88b49..8f9def8 100644 --- a/src/main/java/dev/isxander/controlify/bindings/JoystickButtonBind.java +++ b/src/main/java/dev/isxander/controlify/bindings/JoystickButtonBind.java @@ -31,7 +31,7 @@ public class JoystickButtonBind implements IBind { @Override public void draw(PoseStack matrices, int x, int centerY) { String type = joystick.type().identifier(); - String button = joystick.mapping().button(buttonIndex).identifier(); + String button = joystick.mapping().buttons()[buttonIndex].identifier(); var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/button_" + button + ".png"); RenderSystem.setShaderTexture(0, texture); diff --git a/src/main/java/dev/isxander/controlify/bindings/JoystickHatBind.java b/src/main/java/dev/isxander/controlify/bindings/JoystickHatBind.java index 08d7ccb..254d0aa 100644 --- a/src/main/java/dev/isxander/controlify/bindings/JoystickHatBind.java +++ b/src/main/java/dev/isxander/controlify/bindings/JoystickHatBind.java @@ -33,18 +33,18 @@ public class JoystickHatBind implements IBind { @Override public void draw(PoseStack matrices, int x, int centerY) { String type = joystick.type().identifier(); - String button = joystick.mapping().button(hatIndex).identifier(); + String hat = joystick.mapping().hats()[hatIndex].identifier(); String direction = "centered"; if (hatState.isUp()) direction = "up"; else if (hatState.isDown()) direction = "down"; else if (hatState.isLeft()) - direction = "strong"; + direction = "left"; else if (hatState.isRight()) - direction = "weak"; + direction = "right"; - var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/hat" + button + "_" + direction + ".png"); + var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/hat" + hat + "_" + direction + ".png"); RenderSystem.setShaderTexture(0, texture); RenderSystem.setShaderColor(1, 1, 1, 1); diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerType.java b/src/main/java/dev/isxander/controlify/controller/ControllerType.java index 2cfb255..b365cc9 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerType.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerType.java @@ -11,8 +11,8 @@ import org.quiltmc.json5.JsonReader; import java.io.IOException; import java.util.*; -public record ControllerType(String friendlyName, String identifier) { - public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown"); +public record ControllerType(String friendlyName, String identifier, boolean forceJoystick, boolean dontLoad) { + public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown", false, false); private static Map typeMap = null; private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5"); @@ -47,6 +47,8 @@ public record ControllerType(String friendlyName, String identifier) { while (reader.hasNext()) { String friendlyName = null; String identifier = null; + boolean forceJoystick = false; + boolean dontLoad = false; Set hids = new HashSet<>(); reader.beginObject(); @@ -77,6 +79,8 @@ public record ControllerType(String friendlyName, String identifier) { } reader.endArray(); } + case "force_joystick" -> forceJoystick = reader.nextBoolean(); + case "dont_load" -> dontLoad = reader.nextBoolean(); default -> { Controlify.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping..."); reader.skipValue(); @@ -90,7 +94,7 @@ public record ControllerType(String friendlyName, String identifier) { continue; } - var type = new ControllerType(friendlyName, identifier); + var type = new ControllerType(friendlyName, identifier, forceJoystick, dontLoad); for (var hid : hids) { typeMap.put(hid, type); } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java index 76a1c6d..8e80d48 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java @@ -39,7 +39,7 @@ public class CompoundJoystickController implements JoystickController joystickUids, String friendlyName) { public ControllerType type() { - return new ControllerType(friendlyName, createUID(joystickUids)); + return new ControllerType(friendlyName, createUID(joystickUids), true, false); } public boolean canBeUsed() { diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickConfig.java b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickConfig.java index 5d56b5b..8baf68c 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickConfig.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.controller.joystick; import dev.isxander.controlify.controller.ControllerConfig; +import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; import org.apache.commons.lang3.Validate; import java.util.HashMap; @@ -21,7 +22,7 @@ public class JoystickConfig extends ControllerConfig { if (axis < 0) throw new IllegalArgumentException("Axis cannot be negative!"); - deadzones.put(controller.mapping().axis(axis).identifier(), deadzone); + deadzones.put(controller.mapping().axes()[axis].identifier(), deadzone); } @Override @@ -29,16 +30,18 @@ public class JoystickConfig extends ControllerConfig { if (axis < 0) throw new IllegalArgumentException("Axis cannot be negative!"); - return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f); + return deadzones.getOrDefault(controller.mapping().axes()[axis].identifier(), 0.2f); } void setup(JoystickController controller) { this.controller = controller; if (this.deadzones == null) { deadzones = new HashMap<>(); - for (int i = 0; i < controller.axisCount(); i++) { - if (controller.mapping().axis(i).requiresDeadzone()) - deadzones.put(controller.mapping().axis(i).identifier(), 0.2f); + for (int i = 0; i < controller.mapping().axes().length; i++) { + JoystickMapping.Axis axis = controller.mapping().axes()[i]; + + if (axis.requiresDeadzone()) + deadzones.put(axis.identifier(), 0.2f); } } } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickController.java index da64300..c6f5377 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickController.java @@ -9,8 +9,11 @@ import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMappi public interface JoystickController extends Controller { JoystickMapping mapping(); + @Deprecated int axisCount(); + @Deprecated int buttonCount(); + @Deprecated int hatCount(); @Override diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickState.java b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickState.java index b0ed94d..330101c 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/JoystickState.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/JoystickState.java @@ -1,8 +1,10 @@ package dev.isxander.controlify.controller.joystick; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping; +import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.utils.ControllerUtils; import dev.isxander.yacl.api.NameableEnum; import net.minecraft.network.chat.Component; @@ -11,12 +13,13 @@ import org.lwjgl.glfw.GLFW; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.IntStream; public class JoystickState implements ControllerState { - public static final JoystickState EMPTY = new JoystickState(UnmappedJoystickMapping.INSTANCE, List.of(), List.of(), List.of(), List.of()); + public static final JoystickState EMPTY = new JoystickState(UnmappedJoystickMapping.EMPTY, List.of(), List.of(), List.of(), List.of()); private final JoystickMapping mapping; @@ -54,7 +57,7 @@ public class JoystickState implements ControllerState { @Override public boolean hasAnyInput() { - return IntStream.range(0, axes().size()).anyMatch(i -> !mapping.axis(i).isAxisResting(axes().get(i))) + return IntStream.range(0, axes().size()).anyMatch(i -> !mapping.axes()[i].isAxisResting(axes().get(i))) || buttons().stream().anyMatch(Boolean::booleanValue) || hats().stream().anyMatch(hat -> hat != HatState.CENTERED); } @@ -70,68 +73,85 @@ public class JoystickState implements ControllerState { } public static JoystickState fromJoystick(JoystickController joystick, int joystickId) { + if (DebugProperties.PRINT_JOY_INPUT_COUNT) + Controlify.LOGGER.info("Printing joy input for " + joystick.name()); + FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystickId); - List axes = new ArrayList<>(); - List rawAxes = new ArrayList<>(); - if (axesBuffer != null) { + float[] inAxes = new float[axesBuffer.limit()]; + + if (DebugProperties.PRINT_JOY_INPUT_COUNT) + Controlify.LOGGER.info("Axes count = " + inAxes.length); + + { int i = 0; while (axesBuffer.hasRemaining()) { - var axisMapping = joystick.mapping().axis(i); - var axis = axisMapping.modifyAxis(axesBuffer.get()); - var deadzone = axisMapping.requiresDeadzone(); - - rawAxes.add(axis); - axes.add(deadzone ? ControllerUtils.deadzone(axis, joystick.config().getDeadzone(i)) : axis); - + inAxes[i] = axesBuffer.get(); i++; } } ByteBuffer buttonBuffer = GLFW.glfwGetJoystickButtons(joystickId); - List buttons = new ArrayList<>(); - if (buttonBuffer != null) { + boolean[] inButtons = new boolean[buttonBuffer.limit()]; + + if (DebugProperties.PRINT_JOY_INPUT_COUNT) + Controlify.LOGGER.info("Button count = " + inButtons.length); + + { + int i = 0; while (buttonBuffer.hasRemaining()) { - buttons.add(buttonBuffer.get() == GLFW.GLFW_PRESS); + inButtons[i] = buttonBuffer.get() == GLFW.GLFW_PRESS; + i++; } } ByteBuffer hatBuffer = GLFW.glfwGetJoystickHats(joystickId); - List hats = new ArrayList<>(); - if (hatBuffer != null) { + HatState[] inHats = new HatState[hatBuffer.limit()]; + + if (DebugProperties.PRINT_JOY_INPUT_COUNT) + Controlify.LOGGER.info("Hat count = " + inHats.length); + + { + int i = 0; while (hatBuffer.hasRemaining()) { var state = switch (hatBuffer.get()) { - case GLFW.GLFW_HAT_CENTERED -> JoystickState.HatState.CENTERED; - case GLFW.GLFW_HAT_UP -> JoystickState.HatState.UP; - case GLFW.GLFW_HAT_RIGHT -> JoystickState.HatState.RIGHT; - case GLFW.GLFW_HAT_DOWN -> JoystickState.HatState.DOWN; - case GLFW.GLFW_HAT_LEFT -> JoystickState.HatState.LEFT; - case GLFW.GLFW_HAT_RIGHT_UP -> JoystickState.HatState.RIGHT_UP; - case GLFW.GLFW_HAT_RIGHT_DOWN -> JoystickState.HatState.RIGHT_DOWN; - case GLFW.GLFW_HAT_LEFT_UP -> JoystickState.HatState.LEFT_UP; - case GLFW.GLFW_HAT_LEFT_DOWN -> JoystickState.HatState.LEFT_DOWN; + case GLFW.GLFW_HAT_CENTERED -> HatState.CENTERED; + case GLFW.GLFW_HAT_UP -> HatState.UP; + case GLFW.GLFW_HAT_RIGHT -> HatState.RIGHT; + case GLFW.GLFW_HAT_DOWN -> HatState.DOWN; + case GLFW.GLFW_HAT_LEFT -> HatState.LEFT; + case GLFW.GLFW_HAT_RIGHT_UP -> HatState.RIGHT_UP; + case GLFW.GLFW_HAT_RIGHT_DOWN -> HatState.RIGHT_DOWN; + case GLFW.GLFW_HAT_LEFT_UP -> HatState.LEFT_UP; + case GLFW.GLFW_HAT_LEFT_DOWN -> HatState.LEFT_DOWN; default -> throw new IllegalStateException("Unexpected value: " + hatBuffer.get()); }; - hats.add(state); + inHats[i] = state; } } - return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats); + JoystickMapping.JoystickData data = new JoystickMapping.JoystickData(inAxes, inButtons, inHats); + JoystickMapping mapping = joystick.mapping(); + + JoystickMapping.Axis[] axes = mapping.axes(); + List rawAxes = new ArrayList<>(axes.length); + List deadzoneAxes = new ArrayList<>(axes.length); + for (int i = 0; i < axes.length; i++) { + var axis = axes[i]; + float state = axis.getAxis(data); + rawAxes.add(state); + deadzoneAxes.add(axis.requiresDeadzone() ? ControllerUtils.deadzone(state, i) : state); + } + + List buttons = Arrays.stream(mapping.buttons()).map(button -> button.isPressed(data)).toList(); + List hats = Arrays.stream(mapping.hats()).map(hat -> hat.getHatState(data)).toList(); + + return new JoystickState(joystick.mapping(), deadzoneAxes, rawAxes, buttons, hats); } public static JoystickState empty(JoystickController joystick) { - var axes = new ArrayList(); - var buttons = new ArrayList(); - var hats = new ArrayList(); - - for (int i = 0; i < joystick.axisCount(); i++) { - axes.add(joystick.mapping().axis(i).restingValue()); - } - for (int i = 0; i < joystick.buttonCount(); i++) { - buttons.add(false); - } - for (int i = 0; i < joystick.hatCount(); i++) { - hats.add(HatState.CENTERED); - } + var axes = Arrays.stream(joystick.mapping().axes()).map(JoystickMapping.Axis::restingValue).toList(); + var buttons = IntStream.range(0, joystick.mapping().buttons().length).mapToObj(i -> false).toList(); + var hats = IntStream.range(0, joystick.mapping().hats().length).mapToObj(i -> HatState.CENTERED).toList(); return new JoystickState(joystick.mapping(), axes, axes, buttons, hats); } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java index 25caeb3..222bfe3 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java @@ -13,17 +13,12 @@ import java.util.Objects; public class SingleJoystickController extends AbstractController implements JoystickController { private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY; - private final int axisCount, buttonCount, hatCount; private final JoystickMapping mapping; public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { super(joystickId, hidInfo); - this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity(); - this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity(); - this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity(); - - this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type())); + this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(this)); this.config = new JoystickConfig(this); this.defaultConfig = new JoystickConfig(this); @@ -57,17 +52,17 @@ public class SingleJoystickController extends AbstractController axisMappings; - private final Map buttonMappings; - private final Map hatMappings; + private final AxisMapping[] axes; + private final ButtonMapping[] buttons; + private final HatMapping[] hats; - public RPJoystickMapping(JsonObject object, ControllerType type) { - axisMappings = new HashMap<>(); - object.getAsJsonArray("axes").forEach(element -> { - var axis = element.getAsJsonObject(); - List ids = axis.getAsJsonArray("ids").asList().stream().map(JsonElement::getAsInt).toList(); + public RPJoystickMapping(JsonReader reader, ControllerType type) throws IOException { + AxisMapping[] axes = null; + ButtonMapping[] buttons = null; + HatMapping[] hats = null; - Vec2 inpRange = null; - Vec2 outRange = null; - if (axis.has("range")) { - var rangeElement = axis.get("range"); - if (rangeElement.isJsonArray()) { - var rangeArray = rangeElement.getAsJsonArray(); - outRange = new Vec2(rangeArray.get(0).getAsFloat(), rangeArray.get(1).getAsFloat()); - inpRange = new Vec2(-1, 1); - } else if (rangeElement.isJsonObject()) { - var rangeObject = rangeElement.getAsJsonObject(); - - var inpRangeArray = rangeObject.getAsJsonArray("in"); - inpRange = new Vec2(inpRangeArray.get(0).getAsFloat(), inpRangeArray.get(1).getAsFloat()); - - var outRangeArray = rangeObject.getAsJsonArray("out"); - outRange = new Vec2(outRangeArray.get(0).getAsFloat(), outRangeArray.get(1).getAsFloat()); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "axes" -> { + if (axes != null) + throw new IllegalStateException("Axes defined twice."); + axes = readAxes(reader, type); + } + case "buttons" -> { + if (buttons != null) + throw new IllegalStateException("Buttons defined twice."); + buttons = readButtons(reader, type); + } + case "hats" -> { + if (hats != null) + throw new IllegalStateException("Hats defined twice."); + hats = readHats(reader, type); + } + default -> { + Controlify.LOGGER.warn("Unknown field in joystick mapping: " + name + ". Expected values: ['axes', 'buttons', 'hats']"); + reader.skipValue(); } } - var restState = axis.get("rest").getAsFloat(); - var deadzone = axis.get("deadzone").getAsBoolean(); - var identifier = axis.get("identifier").getAsString(); + } + reader.endObject(); - var axisNames = axis.getAsJsonArray("axis_names").asList().stream() - .map(JsonElement::getAsJsonArray) - .map(JsonArray::asList) - .map(list -> list.stream().map(JsonElement::getAsString).toList()) - .toList(); + this.axes = axes; + this.buttons = buttons; + this.hats = hats; + } + + private AxisMapping[] readAxes(JsonReader reader, ControllerType type) throws IOException { + List axes = new ArrayList<>(); + + reader.beginArray(); + while (reader.hasNext()) { + List ids = new ArrayList<>(); + Vec2 inpRange = null; + Vec2 outRange = null; + boolean deadzone = false; + float restState = 0f; + String identifier = null; + List axisNames = new ArrayList<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "ids" -> { + reader.beginArray(); + while (reader.hasNext()) { + ids.add(reader.nextInt()); + } + reader.endArray(); + } + case "identifier" -> { + identifier = reader.nextString(); + } + case "range" -> { + if (reader.peek() == JsonToken.BEGIN_ARRAY) { + reader.beginArray(); + outRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble()); + inpRange = new Vec2(-1, 1); + reader.endArray(); + } else { + reader.beginObject(); + while (reader.hasNext()) { + String rangeName = reader.nextName(); + + switch (rangeName) { + case "in" -> { + reader.beginArray(); + inpRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble()); + reader.endArray(); + } + case "out" -> { + reader.beginArray(); + outRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble()); + reader.endArray(); + } + default -> { + reader.skipValue(); + Controlify.LOGGER.info("Unknown axis range property: " + rangeName + ". Expected are ['in', 'out']"); + } + } + } + reader.endObject(); + } + } + case "rest" -> { + restState = (float) reader.nextDouble(); + } + case "deadzone" -> { + deadzone = reader.nextBoolean(); + } + case "axis_names" -> { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginArray(); + axisNames.add(new String[] { reader.nextString(), reader.nextString() }); + reader.endArray(); + } + reader.endArray(); + } + default -> { + reader.skipValue(); + Controlify.LOGGER.info("Unknown axis property: " + name + ". Expected are ['identifier', 'axis_names', 'ids', 'range', 'rest', 'deadzone']"); + } + } + } + reader.endObject(); for (var id : ids) { - axisMappings.put(id, new AxisMapping(ids, identifier, inpRange, outRange, restState, deadzone, type.identifier(), axisNames)); + axes.add(new AxisMapping(id, identifier, inpRange, outRange, restState, deadzone, type.identifier(), axisNames.get(ids.indexOf(id)))); } - }); + } + reader.endArray(); - buttonMappings = new HashMap<>(); - object.getAsJsonArray("buttons").forEach(element -> { - var button = element.getAsJsonObject(); - buttonMappings.put(button.get("button").getAsInt(), new ButtonMapping(button.get("name").getAsString(), type.identifier())); - }); + return axes.toArray(new AxisMapping[0]); + } - hatMappings = new HashMap<>(); - object.getAsJsonArray("hats").forEach(element -> { - var hat = element.getAsJsonObject(); - hatMappings.put(hat.get("hat").getAsInt(), new HatMapping(hat.get("name").getAsString(), type.identifier())); - }); + private ButtonMapping[] readButtons(JsonReader reader, ControllerType type) throws IOException { + List buttons = new ArrayList<>(); + + reader.beginArray(); + while (reader.hasNext()) { + int id = -1; + String btnName = null; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "button" -> id = reader.nextInt(); + case "name" -> btnName = reader.nextString(); + default -> { + reader.skipValue(); + Controlify.LOGGER.info("Unknown button property: " + name + ". Expected are ['button', 'name']"); + } + } + } + reader.endObject(); + + buttons.add(new ButtonMapping(id, btnName, type.identifier())); + } + reader.endArray(); + + return buttons.toArray(new ButtonMapping[0]); + } + + private HatMapping[] readHats(JsonReader reader, ControllerType type) throws IOException { + List hats = new ArrayList<>(); + + reader.beginArray(); + while (reader.hasNext()) { + int id = -1; + String hatName = null; + HatMapping.EmulatedAxis axis = null; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "hat" -> id = reader.nextInt(); + case "name" -> hatName = reader.nextString(); + case "emulated_axis" -> { + int axisId = -1; + Map states = new HashMap<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String emulatedName = reader.nextName(); + for (var hatState : JoystickState.HatState.values()) { + if (hatState.name().equalsIgnoreCase(emulatedName)) { + states.put((float) reader.nextDouble(), hatState); + } + } + + if (emulatedName.equalsIgnoreCase("axis")) { + axisId = reader.nextInt(); + } + } + reader.endObject(); + + if (axisId == -1) { + Controlify.LOGGER.error("No axis id defined for emulated hat " + hatName + "! Skipping."); + continue; + } + if (states.size() != JoystickState.HatState.values().length) { + Controlify.LOGGER.error("Not all hat states are defined for emulated hat " + hatName + "! Skipping."); + continue; + } + + axis = new HatMapping.EmulatedAxis(axisId, states); + } + default -> { + reader.skipValue(); + Controlify.LOGGER.info("Unknown hat property: " + name + ". Expected are ['hat', 'name']"); + } + } + } + reader.endObject(); + + hats.add(new HatMapping(id, hatName, type.identifier(), axis)); + } + reader.endArray(); + + return hats.toArray(new HatMapping[0]); } @Override - public Axis axis(int axis) { - if (!axisMappings.containsKey(axis)) - return UnmappedJoystickMapping.INSTANCE.axis(axis); - return axisMappings.get(axis); + public Axis[] axes() { + return axes; } @Override - public Button button(int button) { - if (!buttonMappings.containsKey(button)) - return UnmappedJoystickMapping.INSTANCE.button(button); - return buttonMappings.get(button); + public Button[] buttons() { + return buttons; } @Override - public Hat hat(int hat) { - if (!hatMappings.containsKey(hat)) - return UnmappedJoystickMapping.INSTANCE.hat(hat); - return hatMappings.get(hat); + public Hat[] hats() { + return hats; } - public static JoystickMapping fromType(ControllerType type) { - var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + type.identifier() + ".json")); + public static JoystickMapping fromType(JoystickController joystick) { + var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + joystick.type().identifier() + ".json")); if (resource.isEmpty()) { - Controlify.LOGGER.warn("No joystick mapping found for controller: '" + type.identifier() + "'"); - return UnmappedJoystickMapping.INSTANCE; + Controlify.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().identifier() + "'"); + return new UnmappedJoystickMapping(joystick.joystickId()); } - try (var reader = resource.get().openAsReader()) { - return new RPJoystickMapping(gson.fromJson(reader, JsonObject.class), type); + try (var reader = JsonReader.json5(resource.get().openAsReader())) { + return new RPJoystickMapping(reader, joystick.type()); } catch (Exception e) { - Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + type.identifier() + "'", e); - return UnmappedJoystickMapping.INSTANCE; + Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + joystick.type().identifier() + "'", e); + return new UnmappedJoystickMapping(joystick.joystickId()); } } - private record AxisMapping(List ids, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, List> axisNames) implements Axis { + private record AxisMapping(int id, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, String[] axisNames) implements Axis { @Override - public float modifyAxis(float value) { - if (inpRange() == null || outRange() == null) - return value; + public float getAxis(JoystickData data) { + float rawAxis = data.axes()[id]; - return (value + (outRange().x - inpRange().x)) / (inpRange().y - inpRange().x) * (outRange().y - outRange().x); + if (inpRange() == null || outRange() == null) + return rawAxis; + + return (rawAxis + (outRange().x - inpRange().x)) / (inpRange().y - inpRange().x) * (outRange().y - outRange().x); } @Override @@ -132,23 +291,40 @@ public class RPJoystickMapping implements JoystickMapping { @Override public String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction) { - return this.axisNames().get(ids.indexOf(axis)).get(direction.ordinal()); + return this.axisNames()[direction.ordinal()]; } } - private record ButtonMapping(String identifier, String typeId) implements Button { + private record ButtonMapping(int id, String identifier, String typeId) implements Button { + @Override + public boolean isPressed(JoystickData data) { + return data.buttons()[id]; + } + @Override public Component name() { return Component.translatable("controlify.joystick_mapping." + typeId() + ".button." + identifier()); } - - } - private record HatMapping(String identifier, String typeId) implements Hat { + private record HatMapping(int hatId, String identifier, String typeId, @Nullable EmulatedAxis emulatedAxis) implements Hat { + @Override + public JoystickState.HatState getHatState(JoystickData data) { + if (emulatedAxis() != null) { + var axis = emulatedAxis(); + var axisValue = data.axes()[axis.axisId()]; + return emulatedAxis().states().get(axisValue); + } + + return data.hats()[hatId()]; + } + @Override public Component name() { return Component.translatable("controlify.joystick_mapping." + typeId() + ".hat." + identifier()); } + + private record EmulatedAxis(int axisId, Map states) { + } } } diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/mapping/UnmappedJoystickMapping.java b/src/main/java/dev/isxander/controlify/controller/joystick/mapping/UnmappedJoystickMapping.java index 410adc1..5ae8c53 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/mapping/UnmappedJoystickMapping.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/mapping/UnmappedJoystickMapping.java @@ -1,27 +1,65 @@ package dev.isxander.controlify.controller.joystick.mapping; import dev.isxander.controlify.bindings.JoystickAxisBind; +import dev.isxander.controlify.controller.joystick.JoystickController; +import dev.isxander.controlify.controller.joystick.JoystickState; import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.util.Arrays; public class UnmappedJoystickMapping implements JoystickMapping { - public static final UnmappedJoystickMapping INSTANCE = new UnmappedJoystickMapping(); + public static final UnmappedJoystickMapping EMPTY = new UnmappedJoystickMapping(0, 0, 0); - @Override - public Axis axis(int axis) { - return new UnmappedAxis(axis); + private final UnmappedAxis[] axes; + private final UnmappedButton[] buttons; + private final UnmappedHat[] hats; + + private UnmappedJoystickMapping(int axisCount, int buttonCount, int hatCount) { + this.axes = new UnmappedAxis[axisCount]; + for (int i = 0; i < axisCount; i++) { + this.axes[i] = new UnmappedAxis(i); + } + + this.buttons = new UnmappedButton[axisCount]; + for (int i = 0; i < buttonCount; i++) { + this.buttons[i] = new UnmappedButton(i); + } + + this.hats = new UnmappedHat[hatCount]; + for (int i = 0; i < hatCount; i++) { + this.hats[i] = new UnmappedHat(i); + } + } + + public UnmappedJoystickMapping(int joystickId) { + this( + GLFW.glfwGetJoystickAxes(joystickId).limit(), + GLFW.glfwGetJoystickButtons(joystickId).limit(), + GLFW.glfwGetJoystickHats(joystickId).limit() + ); } @Override - public Button button(int button) { - return new UnmappedButton(button); + public Axis[] axes() { + return axes; } @Override - public Hat hat(int hat) { - return new UnmappedHat(hat); + public Button[] buttons() { + return buttons; + } + + @Override + public Hat[] hats() { + return hats; } private record UnmappedAxis(int axis) implements Axis { + @Override + public float getAxis(JoystickData data) { + return data.axes()[axis]; + } @Override public String identifier() { @@ -38,11 +76,6 @@ public class UnmappedJoystickMapping implements JoystickMapping { return true; } - @Override - public float modifyAxis(float value) { - return value; - } - @Override public boolean isAxisResting(float value) { return value == restingValue(); @@ -60,6 +93,11 @@ public class UnmappedJoystickMapping implements JoystickMapping { } private record UnmappedButton(int button) implements Button { + @Override + public boolean isPressed(JoystickData data) { + return data.buttons()[button]; + } + @Override public String identifier() { return "button-" + button; @@ -72,6 +110,11 @@ public class UnmappedJoystickMapping implements JoystickMapping { } private record UnmappedHat(int hat) implements Hat { + @Override + public JoystickState.HatState getHatState(JoystickData data) { + return data.hats()[hat]; + } + @Override public String identifier() { return "hat-" + hat; diff --git a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java index 04e9228..dd60d9e 100644 --- a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java +++ b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java @@ -1,12 +1,19 @@ package dev.isxander.controlify.debug; -public class DebugProperties { - // Renders debug overlay for vmouse snapping - public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false); - // Forces all gamepads to be treated as a regular joystick - public static final boolean FORCE_JOYSTICK = boolProp("controlify.debug.force_joystick", false); +import net.fabricmc.loader.api.FabricLoader; - private static boolean boolProp(String name, boolean def) { +public class DebugProperties { + /* Renders debug overlay for vmouse snapping */ + public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false, false); + /* Forces all gamepads to be treated as a regular joystick */ + public static final boolean FORCE_JOYSTICK = boolProp("controlify.debug.force_joystick", false, false); + /* Prints joystick input counts for making joystick mappings */ + public static final boolean PRINT_JOY_INPUT_COUNT = boolProp("controlify.debug.print_joy_input_count", false, true); + /* Print gyro data if supported */ + public static final boolean PRINT_GYRO = boolProp("controlify.debug.print_gyro", false, false); + + private static boolean boolProp(String name, boolean defProd, boolean defDev) { + boolean def = FabricLoader.getInstance().isDevelopmentEnvironment() ? defDev : defProd; return Boolean.parseBoolean(System.getProperty(name, Boolean.toString(def))); } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java index 1111518..ede15ce 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java @@ -53,6 +53,8 @@ public class MultiPlayerGameModeMixin { } private void startRumble(BlockState state) { + stopRumble(); + var effect = ContinuousRumbleEffect.builder() .byTick(tick -> new RumbleState( 0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f, diff --git a/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java index b00a624..9d047f9 100644 --- a/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java @@ -1,9 +1,12 @@ package dev.isxander.controlify.rumble; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; import org.apache.commons.lang3.Validate; import java.util.Arrays; import java.util.Objects; +import java.util.function.BooleanSupplier; import java.util.function.Function; public final class BasicRumbleEffect implements RumbleEffect { @@ -11,6 +14,7 @@ public final class BasicRumbleEffect implements RumbleEffect { private int tick = 0; private boolean finished; private int priority = 0; + private BooleanSupplier earlyFinishCondition = () -> false; public BasicRumbleEffect(RumbleState[] keyframes) { this.keyframes = keyframes; @@ -19,7 +23,7 @@ public final class BasicRumbleEffect implements RumbleEffect { @Override public void tick() { tick++; - if (tick >= keyframes.length) + if (tick >= keyframes.length || earlyFinishCondition.getAsBoolean()) finished = true; } @@ -55,6 +59,12 @@ public final class BasicRumbleEffect implements RumbleEffect { return keyframes; } + public BasicRumbleEffect earlyFinish(BooleanSupplier condition) { + var current = earlyFinishCondition; + this.earlyFinishCondition = () -> current.getAsBoolean() || condition.getAsBoolean(); + return this; + } + @Override public boolean equals(Object obj) { if (obj == this) return true; @@ -76,6 +86,22 @@ public final class BasicRumbleEffect implements RumbleEffect { "priority=" + this.priority() + ']'; } + public BasicRumbleEffect join(BasicRumbleEffect other) { + return BasicRumbleEffect.join(this, other); + } + + public BasicRumbleEffect repeat(int count) { + Validate.isTrue(count > 0, "count must be greater than 0"); + + if (count == 1) return this; + + BasicRumbleEffect effect = this; + for (int i = 0; i < count - 1; i++) { + effect = BasicRumbleEffect.join(effect, this); + } + return effect; + } + /** * Creates a rumble effect where the state is determined by the tick. * @@ -133,19 +159,8 @@ public final class BasicRumbleEffect implements RumbleEffect { return new BasicRumbleEffect(states); } - public BasicRumbleEffect join(BasicRumbleEffect other) { - return BasicRumbleEffect.join(this, other); - } - - public BasicRumbleEffect repeat(int count) { - Validate.isTrue(count > 0, "count must be greater than 0"); - - if (count == 1) return this; - - BasicRumbleEffect effect = this; - for (int i = 0; i < count - 1; i++) { - effect = BasicRumbleEffect.join(effect, this); - } - return effect; + public static BooleanSupplier finishOnScreenChange() { + Screen screen = Minecraft.getInstance().screen; + return () -> screen != Minecraft.getInstance().screen; } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index d951c82..e6ac52a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -31,7 +31,7 @@ "fabricloader": ">=0.14.0", "minecraft": "~1.19.4", "java": ">=17", - "yet-another-config-lib": ">=2.4.0" + "yet-another-config-lib": "^2.4.0" }, "breaks": { "midnightcontrols": "*"