diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be0eab2..bb5b579 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ quilt_json5 = "1.0.3" sodium = "mc1.19.4-0.4.10" iris = "1.5.2+1.19.4" immediately_fast = "1.1.10+1.19.4" -sdl2_jni = "2.26.4-4" +sdl2_jni = "2.26.5-18" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 1f0bea5..4b0893a 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -7,11 +7,10 @@ import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint; import dev.isxander.controlify.config.gui.ControllerBindHandler; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; -import dev.isxander.controlify.controller.joystick.CompoundJoystickController; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; -import dev.isxander.controlify.gui.screen.VibrationOnboardingScreen; +import dev.isxander.controlify.gui.screen.SDLOnboardingScreen; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.config.ControlifyConfig; import dev.isxander.controlify.controller.hid.ControllerHIDService; @@ -127,7 +126,7 @@ public class Controlify implements ControlifyApi { // find already connected controllers for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) { if (GLFW.glfwJoystickPresent(jid)) { - var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType()); + var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid)); if (controllerOpt.isEmpty()) continue; var controller = controllerOpt.get(); @@ -145,14 +144,12 @@ public class Controlify implements ControlifyApi { } } - checkCompoundJoysticks(); - - if (Controller.CONTROLLERS.isEmpty()) { + if (ControllerManager.getConnectedControllers().isEmpty()) { LOGGER.info("No controllers found."); } if (getCurrentController().isEmpty() && config().isFirstLaunch()) { - this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); + this.setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null)); } else { // setCurrentController saves config config().saveIfDirty(); @@ -213,7 +210,7 @@ public class Controlify implements ControlifyApi { boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive(); - for (var controller : Controller.CONTROLLERS.values()) { + for (var controller : ControllerManager.getConnectedControllers()) { if (!outOfFocus) wrapControllerError(controller::updateState, "Updating controller state", controller); else @@ -295,7 +292,7 @@ public class Controlify implements ControlifyApi { } private void onControllerHotplugged(int jid) { - var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType()); + var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid)); if (controllerOpt.isEmpty()) return; var controller = controllerOpt.get(); @@ -308,9 +305,7 @@ public class Controlify implements ControlifyApi { config().setDirty(); } - checkCompoundJoysticks(); - - if (Controller.CONTROLLERS.size() == 1) { + if (ControllerManager.getConnectedControllers().size() == 1) { this.setCurrentController(controller); ToastUtils.sendToast( @@ -325,12 +320,12 @@ public class Controlify implements ControlifyApi { } private void onControllerDisconnect(int jid) { - Controller.CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> { - Controller.remove(controller); + ControllerManager.getConnectedControllers().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> { + ControllerManager.disconnect(controller); controller.hidInfo().ifPresent(controllerHIDService::unconsumeController); - setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); + setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null)); LOGGER.info("Controller disconnected: " + controller.name()); this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); @@ -340,28 +335,6 @@ public class Controlify implements ControlifyApi { false ); }); - - checkCompoundJoysticks(); - } - - private void checkCompoundJoysticks() { - config().getCompoundJoysticks().values().forEach(info -> { - try { - if (info.isLoaded() && !info.canBeUsed()) { - LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers."); - Controller.CONTROLLERS.remove(info.type().mappingId()); - } - - if (!info.isLoaded() && info.canBeUsed()) { - LOGGER.info("Loading compound joystick " + info.type().mappingId() + "."); - CompoundJoystickController controller = info.attemptCreate().orElseThrow(); - Controller.CONTROLLERS.put(info.type().mappingId(), controller); - config().loadOrCreateControllerData(controller); - } - } catch (Exception e) { - e.printStackTrace(); - } - }); } private void askToSwitchController(Controller controller) { diff --git a/src/main/java/dev/isxander/controlify/ControllerManager.java b/src/main/java/dev/isxander/controlify/ControllerManager.java new file mode 100644 index 0000000..210459a --- /dev/null +++ b/src/main/java/dev/isxander/controlify/ControllerManager.java @@ -0,0 +1,107 @@ +package dev.isxander.controlify; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.gamepad.GamepadController; +import dev.isxander.controlify.controller.hid.ControllerHIDService; +import dev.isxander.controlify.controller.joystick.CompoundJoystickController; +import dev.isxander.controlify.controller.joystick.SingleJoystickController; +import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.utils.DebugLog; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import org.hid4java.HidDevice; +import org.lwjgl.glfw.GLFW; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public final class ControllerManager { + private ControllerManager() { + } + + private final static Map> CONTROLLERS = new HashMap<>(); + + public static Optional> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { + try { + Optional uid = hidInfo.createControllerUID(); + if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { + return Optional.of(CONTROLLERS.get(uid.get())); + } + + if (hidInfo.type().dontLoad()) { + DebugLog.log("Preventing load of controller #" + joystickId + " because its type prevents loading."); + return Optional.empty(); + } + + if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) { + GamepadController controller = new GamepadController(joystickId, hidInfo); + CONTROLLERS.put(controller.uid(), controller); + checkCompoundJoysticks(); + return Optional.of(controller); + } + + SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo); + CONTROLLERS.put(controller.uid(), controller); + checkCompoundJoysticks(); + return Optional.of(controller); + } catch (Throwable e) { + CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickId); + CrashReportCategory category = crashReport.addCategory("Controller Info"); + category.setDetail("Joystick ID", joystickId); + category.setDetail("Controller identification", hidInfo.type()); + category.setDetail("HID path", hidInfo.hidDevice().map(HidDevice::getPath).orElse("N/A")); + category.setDetail("HID service status", Controlify.instance().controllerHIDService().isDisabled() ? "Disabled" : "Enabled"); + category.setDetail("GLFW name", Optional.ofNullable(GLFW.glfwGetJoystickName(joystickId)).orElse("N/A")); + throw new ReportedException(crashReport); + } + } + + public static void disconnect(Controller controller) { + controller.close(); + CONTROLLERS.remove(controller.uid(), controller); + + checkCompoundJoysticks(); + } + + public static void disconnect(String uid) { + Controller prev = CONTROLLERS.remove(uid); + if (prev != null) { + prev.close(); + } + + checkCompoundJoysticks(); + } + + public static List> getConnectedControllers() { + return ImmutableList.copyOf(CONTROLLERS.values()); + } + + public static boolean isControllerConnected(String uid) { + return CONTROLLERS.containsKey(uid); + } + + private static void checkCompoundJoysticks() { + Controlify.instance().config().getCompoundJoysticks().values().forEach(info -> { + try { + if (info.isLoaded() && !info.canBeUsed()) { + Controlify.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers."); + disconnect(info.type().mappingId()); + } + + if (!info.isLoaded() && info.canBeUsed()) { + Controlify.LOGGER.info("Loading compound joystick " + info.type().mappingId() + "."); + CompoundJoystickController controller = info.attemptCreate().orElseThrow(); + CONTROLLERS.put(info.type().mappingId(), controller); + Controlify.instance().config().loadOrCreateControllerData(controller); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index 1afccc9..26b81e8 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.config; import com.google.gson.*; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo; import dev.isxander.controlify.utils.DebugLog; @@ -78,7 +79,7 @@ public class ControlifyConfig { JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data - for (var controller : Controller.CONTROLLERS.values()) { + for (var controller : ControllerManager.getConnectedControllers()) { // `add` replaces if already existing newControllerData.add(controller.uid(), generateControllerConfig(controller)); } @@ -111,7 +112,7 @@ public class ControlifyConfig { JsonObject controllers = object.getAsJsonObject("controllers"); if (controllers != null) { this.controllerData = controllers; - for (var controller : Controller.CONTROLLERS.values()) { + for (var controller : ControllerManager.getConnectedControllers()) { loadOrCreateControllerData(controller); } } else { diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index 32aca7b..59d5a0c 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -2,9 +2,11 @@ package dev.isxander.controlify.config.gui; import com.google.common.collect.Iterables; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.api.bind.ControllerBinding; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.config.GlobalSettings; +import dev.isxander.controlify.controller.BatteryLevel; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerConfig; import dev.isxander.controlify.controller.ControllerState; @@ -12,7 +14,9 @@ import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme; import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; +import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; +import dev.isxander.controlify.gui.screen.SDLOnboardingScreen; import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.RumbleSource; @@ -58,7 +62,7 @@ public class YACLHelper { .name(Component.translatable("controlify.gui.current_controller")) .tooltip(Component.translatable("controlify.gui.current_controller.tooltip")) .binding(Controlify.instance().getCurrentController().orElse(Controller.DUMMY), () -> Controlify.instance().getCurrentController().orElse(Controller.DUMMY), v -> Controlify.instance().setCurrentController(v)) - .controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), Controller.CONTROLLERS.values().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name()))) + .controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), ControllerManager.getConnectedControllers().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name()))) .build()) .option(globalVibrationOption = Option.createBuilder(boolean.class) .name(Component.translatable("controlify.gui.load_vibration_natives")) @@ -102,7 +106,7 @@ public class YACLHelper { yacl.category(globalCategory.build()); - for (var controller : Controller.CONTROLLERS.values()) { + for (var controller : ControllerManager.getConnectedControllers()) { yacl.category(createControllerCategory(controller, globalVibrationOption)); } diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index 0407a7c..5b47487 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -4,15 +4,10 @@ import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.JsonElement; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.InputMode; -import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.hid.ControllerHIDService; -import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.rumble.RumbleCapable; -import dev.isxander.controlify.rumble.RumbleManager; -import dev.isxander.controlify.rumble.RumbleSource; -import org.libsdl.SDL; import org.lwjgl.glfw.GLFW; import java.util.Objects; @@ -63,7 +58,7 @@ public abstract class AbstractController 1000) throw new IllegalStateException("Could not find a unique name for controller " + name + " (" + uid() + ")! (tried " + i + " times)"); } diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index 1431c86..1cef661 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -54,46 +54,6 @@ public interface Controller> CONTROLLERS = new HashMap<>(); - - static Optional> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { - try { - Optional uid = hidInfo.createControllerUID(); - if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { - return Optional.of(CONTROLLERS.get(uid.get())); - } - - if (hidInfo.type().dontLoad()) { - DebugLog.log("Preventing load of controller #" + joystickId + " because its type prevents loading."); - return Optional.empty(); - } - - if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) { - GamepadController controller = new GamepadController(joystickId, hidInfo); - CONTROLLERS.put(controller.uid(), controller); - return Optional.of(controller); - } - - SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo); - CONTROLLERS.put(controller.uid(), controller); - return Optional.of(controller); - } catch (Throwable e) { - CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickId); - CrashReportCategory category = crashReport.addCategory("Controller Info"); - category.setDetail("Joystick ID", joystickId); - category.setDetail("Controller identification", hidInfo.type()); - category.setDetail("HID path", hidInfo.hidDevice().map(HidDevice::getPath).orElse("N/A")); - category.setDetail("HID service status", Controlify.instance().controllerHIDService().isDisabled() ? "Disabled" : "Enabled"); - category.setDetail("GLFW name", Optional.ofNullable(GLFW.glfwGetJoystickName(joystickId)).orElse("N/A")); - throw new ReportedException(crashReport); - } - } - - static void remove(Controller controller) { - controller.close(); - CONTROLLERS.remove(controller.uid(), controller); - } - @Deprecated Controller DUMMY = new Controller<>() { private final ControllerBindings bindings = new ControllerBindings<>(this); diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickInfo.java b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickInfo.java index b0d3af2..9cae552 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickInfo.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickInfo.java @@ -1,5 +1,6 @@ package dev.isxander.controlify.controller.joystick; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerType; @@ -13,7 +14,7 @@ public record CompoundJoystickInfo(Collection joystickUids, String frien } public boolean canBeUsed() { - List> joysticks = Controller.CONTROLLERS.values().stream().filter(c -> joystickUids.contains(c.uid())).toList(); + List> joysticks = ControllerManager.getConnectedControllers().stream().filter(c -> joystickUids.contains(c.uid())).toList(); if (joysticks.size() != joystickUids().size()) { return false; // not all controllers are connected } @@ -25,13 +26,13 @@ public record CompoundJoystickInfo(Collection joystickUids, String frien } public boolean isLoaded() { - return Controller.CONTROLLERS.containsKey(createUID(joystickUids)); + return ControllerManager.isControllerConnected(createUID(joystickUids)); } public Optional attemptCreate() { if (!canBeUsed()) return Optional.empty(); - List joystickIDs = Controller.CONTROLLERS.values().stream() + List joystickIDs = ControllerManager.getConnectedControllers().stream() .filter(c -> joystickUids.contains(c.uid())) .map(Controller::joystickId) .toList(); diff --git a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java index 18f2685..4c5da71 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -2,14 +2,13 @@ package dev.isxander.controlify.mixins.core; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.gui.screen.BetaNoticeScreen; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.gui.components.toasts.ToastComponent; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.main.GameConfig; -import net.minecraft.network.chat.Component; import net.minecraft.server.packs.resources.ReloadInstance; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; @@ -54,18 +53,8 @@ public abstract class MinecraftMixin { setScreen(new BetaNoticeScreen()); } - @ModifyExpressionValue(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/ReloadableResourceManager;createReload(Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Ljava/util/List;)Lnet/minecraft/server/packs/resources/ReloadInstance;")) - private ReloadInstance onReloadResources(ReloadInstance resourceReload) { - resourceReload.done().thenRun(() -> { - if (Controlify.instance().controllerHIDService().isDisabled()) { - getToasts().addToast(SystemToast.multiline((Minecraft) (Object) this, SystemToast.SystemToastIds.UNSECURE_SERVER_WARNING, Component.translatable("controlify.error.hid"), Component.translatable("controlify.error.hid.desc"))); - } - }); - return resourceReload; - } - @Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/telemetry/ClientTelemetryManager;close()V")) private void onMinecraftClose(CallbackInfo ci) { - Controller.CONTROLLERS.values().forEach(Controller::close); + ControllerManager.getConnectedControllers().forEach(Controller::close); } }