From b07066e09768c1eecfed7f96f4f1cf08efa2b117 Mon Sep 17 00:00:00 2001 From: isXander Date: Fri, 3 Nov 2023 17:17:50 +0000 Subject: [PATCH] [BROKEN] Abstract controller manager system --- .../dev/isxander/controlify/Controlify.java | 307 ++++++------------ .../api/event/ControlifyEvents.java | 23 ++ .../controlify/config/ControlifyConfig.java | 18 +- .../controlify/config/GlobalSettings.java | 2 +- .../controller/AbstractController.java | 3 +- .../controller/gamepad/GamepadController.java | 10 +- .../joystick/CompoundJoystickInfo.java | 8 +- .../AbstractControllerManager.java} | 91 +++++- .../controllermanager/ControllerManager.java | 24 ++ .../GLFWControllerManager.java | 96 ++++++ .../SDLControllerManager.java | 130 ++++++++ .../controlify/debug/DebugProperties.java | 2 +- .../controlify/driver/SDL2NativesManager.java | 2 +- .../driver/gamepad/SDL2GamepadDriver.java | 4 + .../screen/ControllerCalibrationScreen.java | 12 +- .../gui/screen/ControllerCarouselScreen.java | 31 +- .../gui/screen/ModConfigOpenerScreen.java | 6 +- .../mixins/core/MinecraftMixin.java | 5 +- .../chatkbheight/ChatComponentMixin.java | 10 +- .../settingsbutton/ControlsScreenMixin.java | 5 +- .../controlify/utils/ControllerUtils.java | 16 + .../wireless/LowBatteryNotifier.java | 8 +- ...itch_face_button_down.png => a_button.png} | Bin ...tch_face_button_right.png => b_button.png} | Bin .../{switch_select_button.png => back.png} | Bin .../{switch_dpad_down.png => dpad_down.png} | Bin .../{switch_dpad_left.png => dpad_left.png} | Bin .../{switch_dpad_right.png => dpad_right.png} | Bin .../{switch_dpad_up.png => dpad_up.png} | Bin ...switch_bumper_left.png => left_bumper.png} | Bin ...tch_stick_left.png => left_stick_down.png} | Bin ...itch_left_trigger.png => left_trigger.png} | Bin ...itch_bumper_right.png => right_bumper.png} | Bin ...h_stick_right.png => right_stick_down.png} | Bin ...ch_right_trigger.png => right_trigger.png} | Bin .../{switch_start_button.png => start.png} | Bin ...itch_face_button_left.png => x_button.png} | Bin ...switch_face_button_up.png => y_button.png} | Bin .../controlify/test/FakeController.java | 4 +- 39 files changed, 531 insertions(+), 286 deletions(-) rename src/main/java/dev/isxander/controlify/{ControllerManager.java => controllermanager/AbstractControllerManager.java} (52%) create mode 100644 src/main/java/dev/isxander/controlify/controllermanager/ControllerManager.java create mode 100644 src/main/java/dev/isxander/controlify/controllermanager/GLFWControllerManager.java create mode 100644 src/main/java/dev/isxander/controlify/controllermanager/SDLControllerManager.java rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_face_button_down.png => a_button.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_face_button_right.png => b_button.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_select_button.png => back.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_dpad_down.png => dpad_down.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_dpad_left.png => dpad_left.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_dpad_right.png => dpad_right.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_dpad_up.png => dpad_up.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_bumper_left.png => left_bumper.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_stick_left.png => left_stick_down.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_left_trigger.png => left_trigger.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_bumper_right.png => right_bumper.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_stick_right.png => right_stick_down.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_right_trigger.png => right_trigger.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_start_button.png => start.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_face_button_left.png => x_button.png} (100%) rename src/main/resources/assets/controlify/textures/gui/gamepad/switch/{switch_face_button_up.png => y_button.png} (100%) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 44d914c..4d7a860 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -1,13 +1,14 @@ package dev.isxander.controlify; -import com.google.common.io.ByteStreams; import com.mojang.blaze3d.Blaze3D; -import com.sun.jna.Memory; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint; import dev.isxander.controlify.compatibility.ControlifyCompat; import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping; +import dev.isxander.controlify.controllermanager.ControllerManager; +import dev.isxander.controlify.controllermanager.GLFWControllerManager; +import dev.isxander.controlify.controllermanager.SDLControllerManager; import dev.isxander.controlify.gui.controllers.ControllerBindHandler; import dev.isxander.controlify.gui.screen.*; import dev.isxander.controlify.controller.Controller; @@ -26,7 +27,6 @@ import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor; import dev.isxander.controlify.utils.*; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import dev.isxander.controlify.wireless.LowBatteryNotifier; -import io.github.libsdl4j.api.rwops.SDL_RWops; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; @@ -40,30 +40,28 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import org.lwjgl.system.MemoryUtil; -import java.io.InputStream; -import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.List; import java.util.Optional; import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.stream.IntStream; -import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerAddMappingsFromRW; -import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerNumMappings; -import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem; +import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError; public class Controlify implements ControlifyApi { private static Controlify instance = null; private final Minecraft minecraft = Minecraft.getInstance(); + private ControllerManager controllerManager; + + private boolean finishedInit = false; + private boolean probeMode = false; + private Controller currentController = null; private InGameInputHandler inGameInputHandler; public InGameButtonGuide inGameButtonGuide; @@ -155,18 +153,13 @@ public class Controlify implements ControlifyApi { config().load(); - // initialise and compatability modules that controlify implements itself - // this does NOT invoke any entrypoints. this is done in the pre-initialisation phase - ControlifyCompat.init(); + boolean controllersConnected = GLFWControllerManager.areControllersConnected(); - var controllersConnected = IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1) - .anyMatch(GLFW::glfwJoystickPresent); - if (controllersConnected) { // only initialise Controlify if controllers are detected - if (!config().globalSettings().delegateSetup) { - // check native onboarding then discover controllers - askNatives().whenComplete((loaded, th) -> discoverControllers()); - } else { - // delegate setup: don't auto set up controllers, require the user to open config screen + ControlifyEvents.CONTROLLER_CONNECTED.register(this::onControllerAdded); + ControlifyEvents.CONTROLLER_DISCONNECTED.register(this::onControllerRemoved); + + if (controllersConnected) { + if (config().globalSettings().quietMode) { ToastUtils.sendToast( Component.translatable("controlify.toast.setup_in_config.title"), Component.translatable( @@ -177,33 +170,22 @@ public class Controlify implements ControlifyApi { ), false ); + } else { + finishControlifyInit(); } + } else { + probeMode = true; + ClientTickEvents.END_CLIENT_TICK.register(client -> this.probeTick()); } + // initialise and compatability modules that controlify implements itself + // this does NOT invoke any entrypoints. this is done in the pre-initialisation phase + ControlifyCompat.init(); + // register events - ClientTickEvents.START_CLIENT_TICK.register(this::tick); ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> { controllerHIDService().stop(); }); - ConnectServerEvent.EVENT.register((minecraft, address, data) -> { - notifyNewServer(data); - }); - - // set up the hotplugging callback with GLFW - // TODO: investigate if there is any benefit to implementing this with SDL - GLFW.glfwSetJoystickCallback((jid, event) -> { - try { - this.askNatives().whenComplete((loaded, th) -> { - if (event == GLFW.GLFW_CONNECTED) { - this.onControllerHotplugged(jid); - } else if (event == GLFW.GLFW_DISCONNECTED) { - this.onControllerDisconnect(jid); - } - }); - } catch (Throwable e) { - Log.LOGGER.error("Failed to handle controller connect/disconnect event", e); - } - }); // sends toasts of new features notifyOfNewFeatures(); @@ -217,73 +199,26 @@ public class Controlify implements ControlifyApi { public void discoverControllers() { if (hasDiscoveredControllers) { Log.LOGGER.warn("Attempted to discover controllers twice!"); + return; } hasDiscoveredControllers = true; DebugLog.log("Discovering and initializing controllers..."); - // load gamepad mappings before every - minecraft.getResourceManager() - .getResource(Controlify.id("controllers/gamecontrollerdb.txt")) - .ifPresent(this::loadGamepadMappings); + controllerManager.discoverControllers(); - // find already connected controllers - // TODO: investigate if there is any benefit to implementing this with SDL - for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) { - if (GLFW.glfwJoystickPresent(jid)) { - Optional> controllerOpt = ControllerManager.createOrGet( - jid, - controllerHIDService.fetchType(jid) - ); - if (controllerOpt.isEmpty()) - continue; - Controller controller = controllerOpt.get(); - - Log.LOGGER.info("Controller found: " + ControllerUtils.createControllerString(controller)); - - boolean newController = !config().loadOrCreateControllerData(controller); - - if (SubmitUnknownControllerScreen.canSubmit(controller)) { - minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen)); - } - - // only "equip" the controller if it has already been calibrated - if (!controller.config().deadzonesCalibrated) { - calibrationQueue.add(controller); - } else if (controller.uid().equals(config().currentControllerUid())) { - setCurrentController(controller); - } - - // make sure that allow vibrations is not mismatched with the native library setting - if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { - controller.config().allowVibrations = false; - config().setDirty(); - } - - // if a joystick and unmapped, tell the user that they need to configure the controls - // joysticks have an abstract number of inputs, so applying a default control scheme is impossible - if (newController && controller instanceof JoystickController joystick && joystick.mapping() instanceof UnmappedJoystickMapping) { - ToastUtils.sendToast( - Component.translatable("controlify.toast.unmapped_joystick.title"), - Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()), - true - ); - } - } - } - - if (ControllerManager.getConnectedControllers().isEmpty()) { + if (controllerManager.getConnectedControllers().isEmpty()) { Log.LOGGER.info("No controllers found."); } // if no controller is currently selected, select the first one if (getCurrentController().isEmpty()) { - var controller = ControllerManager.getConnectedControllers().stream().findFirst().orElse(null); + Controller controller = controllerManager.getConnectedControllers().stream().findFirst().orElse(null); if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) { controller = null; } - this.setCurrentController(controller); + this.setCurrentController(controller, false); } else { // setCurrentController saves config so there is no need to set dirty to save config().saveIfDirty(); @@ -298,51 +233,57 @@ public class Controlify implements ControlifyApi { }); } - /** - * Called when a controller has been connected after mod initialisation. - * If this is the first controller to be connected in the game's lifecycle, - * this is delegated to {@link Controlify#discoverControllers()} for it to be "discovered", - * otherwise the controller is initialised and added to the list of connected controllers. - */ - private void onControllerHotplugged(int jid) { - if (!hasDiscoveredControllers) { - discoverControllers(); - return; + public CompletableFuture finishControlifyInit() { + if (finishedInit) { + return CompletableFuture.completedFuture(null); } + finishedInit = true; - var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid)); - if (controllerOpt.isEmpty()) return; - var controller = controllerOpt.get(); + askNatives().whenComplete((loaded, th) -> { + Log.LOGGER.info("Finishing Controlify init..."); - Log.LOGGER.info("Controller connected: " + ControllerUtils.createControllerString(controller)); + controllerManager = loaded ? new SDLControllerManager() : new GLFWControllerManager(); - config().loadOrCreateControllerData(controller); + ClientTickEvents.START_CLIENT_TICK.register(this::tick); + ConnectServerEvent.EVENT.register((minecraft, address, data) -> { + notifyNewServer(data); + }); + discoverControllers(); + }); + return askNatives().thenApply(loaded -> null); + } + + private void onControllerAdded(Controller controller, boolean hotplugged, boolean newController) { if (SubmitUnknownControllerScreen.canSubmit(controller)) { minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen)); } - if (config().globalSettings().delegateSetup) { - config().globalSettings().delegateSetup = false; - config().setDirty(); - } - - if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { + if (controller.config().allowVibrations && !SDL2NativesManager.isLoaded()) { controller.config().allowVibrations = false; config().setDirty(); } - if (ControllerManager.getConnectedControllers().size() == 1 && (controller.config().deadzonesCalibrated || controller.config().delayedCalibration)) { - this.setCurrentController(controller); + if (hotplugged) { + if (controller.config().deadzonesCalibrated) { + setCurrentController(controller, hotplugged); + } else { + calibrationQueue.add(controller); + } + } + if (controller instanceof JoystickController joystick && joystick.mapping() instanceof UnmappedJoystickMapping) { ToastUtils.sendToast( - Component.translatable("controlify.toast.default_controller_connected.title"), - Component.translatable("controlify.toast.default_controller_connected.description"), - false + Component.translatable("controlify.toast.unmapped_joystick.title"), + Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()), + true ); } else { - this.askToSwitchController(controller); - config().saveIfDirty(); + ToastUtils.sendToast( + Component.translatable("controlify.toast.controller_connected.title"), + Component.translatable("controlify.toast.controller_connected.description", controller.name()), + false + ); } if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) { @@ -350,32 +291,25 @@ public class Controlify implements ControlifyApi { } } - /** - * Called when a controller is disconnected. - * Equips another controller if available. - * - * @param jid the joystick id of the disconnected controller - */ - private void onControllerDisconnect(int jid) { - ControllerManager.getConnectedControllers().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> { - ControllerManager.disconnect(controller); + private void onControllerRemoved(Controller controller) { + this.setCurrentController( + controllerManager.getConnectedControllers() + .stream() + .findFirst() + .orElse(null), + true); - controller.hidInfo().ifPresent(controllerHIDService::unconsumeController); + this.setInputMode( + getCurrentController().isEmpty() + ? InputMode.KEYBOARD_MOUSE + : InputMode.CONTROLLER + ); - setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null)); - Log.LOGGER.info("Controller disconnected: " + controller.name()); - this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); - - ToastUtils.sendToast( - Component.translatable("controlify.toast.controller_disconnected.title"), - Component.translatable("controlify.toast.controller_disconnected.description", controller.name()), - false - ); - - if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) { - controllerListScreen.refreshControllers(); - } - }); + ToastUtils.sendToast( + Component.translatable("controlify.toast.controller_disconnected.title"), + Component.translatable("controlify.toast.controller_disconnected.description", controller.name()), + false + ); } /** @@ -422,36 +356,6 @@ public class Controlify implements ControlifyApi { return nativeOnboardingFuture; } - /** - * Loads the gamepad mappings for both GLFW and SDL2. - * @param resource the already located `gamecontrollerdb.txt` resource - */ - private void loadGamepadMappings(Resource resource) { - Log.LOGGER.debug("Loading gamepad mappings..."); - - try (InputStream is = resource.open()) { - byte[] bytes = ByteStreams.toByteArray(is); - - ByteBuffer buffer = MemoryUtil.memASCIISafe(new String(bytes)); - if (!GLFW.glfwUpdateGamepadMappings(buffer)) { - Log.LOGGER.error("GLFW failed to load gamepad mappings!"); - } - - if (SDL2NativesManager.isLoaded()) { - try (Memory memory = new Memory(bytes.length)) { - memory.write(0, bytes, 0, bytes.length); - SDL_RWops rw = SDL_RWFromConstMem(memory, (int) memory.size()); - int count = SDL_GameControllerAddMappingsFromRW(rw, 1); - if (count < 1) { - Log.LOGGER.error("SDL2 failed to load gamepad mappings!"); - } - } - } - } catch (Throwable e) { - Log.LOGGER.error("Failed to load gamecontrollerdb.txt", e); - } - } - /** * The main loop of Controlify. * In Controlify's current state, only the current controller is ticked. @@ -469,18 +373,12 @@ public class Controlify implements ControlifyApi { boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive(); - for (var controller : ControllerManager.getConnectedControllers()) { - if (!outOfFocus) - wrapControllerError(controller::updateState, "Updating controller state", controller); - else - wrapControllerError(controller::clearState, "Clearing controller state", controller); - ControlifyEvents.CONTROLLER_STATE_UPDATE.invoker().onControllerStateUpdate(controller); - } + controllerManager.tick(outOfFocus); if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) { if (switchableController.state().hasAnyInput()) { switchableController.clearState(); - this.setCurrentController(switchableController); // setCurrentController sets switchableController to null + this.setCurrentController(switchableController, true); // setCurrentController sets switchableController to null if (askSwitchToast != null) { askSwitchToast.remove(); askSwitchToast = null; @@ -533,7 +431,7 @@ public class Controlify implements ControlifyApi { Component.translatable("controlify.toast.faulty_input.description"), true ); - this.setCurrentController(null); + this.setCurrentController(null, true); consecutiveInputSwitches = 0; return; } @@ -549,16 +447,12 @@ public class Controlify implements ControlifyApi { ControlifyEvents.ACTIVE_CONTROLLER_TICKED.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); + private void probeTick() { + if (probeMode) { + if (GLFWControllerManager.areControllersConnected()) { + probeMode = false; + minecraft.execute(this::finishControlifyInit); + } } } @@ -566,17 +460,6 @@ public class Controlify implements ControlifyApi { return config; } - private void askToSwitchController(Controller controller) { - this.switchableController = controller; - this.askSwitchTime = Blaze3D.getTime(); - - askSwitchToast = ToastUtils.sendToast( - Component.translatable("controlify.toast.ask_to_switch.title"), - Component.translatable("controlify.toast.ask_to_switch.description", controller.name()), - true - ); - } - @Override @Deprecated public @NotNull Controller currentController() { @@ -591,7 +474,7 @@ public class Controlify implements ControlifyApi { return Optional.ofNullable(currentController); } - public void setCurrentController(@Nullable Controller controller) { + public void setCurrentController(@Nullable Controller controller, boolean changeInputMode) { if (this.currentController == controller) return; this.currentController = controller; @@ -617,10 +500,14 @@ public class Controlify implements ControlifyApi { this.inGameInputHandler = new InGameInputHandler(controller); - setInputMode(controller.config().mixedInput ? InputMode.MIXED : InputMode.CONTROLLER); + if (controller.config().mixedInput) + setInputMode(InputMode.MIXED); + else if (changeInputMode) + setInputMode(InputMode.CONTROLLER); + } - if (!controller.config().deadzonesCalibrated) - calibrationQueue.add(controller); + public Optional getControllerManager() { + return Optional.ofNullable(controllerManager); } public Optional inGameInputHandler() { diff --git a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java index e4a87e7..bad1409 100644 --- a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java +++ b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java @@ -5,10 +5,23 @@ import dev.isxander.controlify.api.ingameinput.LookInputModifier; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry; +import dev.isxander.controlify.controllermanager.ControllerManager; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; public final class ControlifyEvents { + public static final Event CONTROLLER_CONNECTED = EventFactory.createArrayBacked(ControllerConnected.class, callbacks -> (controller, hotplugged, newController) -> { + for (ControllerConnected callback : callbacks) { + callback.onControllerConnected(controller, hotplugged, newController); + } + }); + + public static final Event CONTROLLER_DISCONNECTED = EventFactory.createArrayBacked(ControllerDisconnected.class, callbacks -> controller -> { + for (ControllerDisconnected callback : callbacks) { + callback.onControllerDisconnected(controller); + } + }); + /** * Triggers when the input mode is changed from keyboard to controller or vice versa. */ @@ -82,6 +95,16 @@ public final class ControlifyEvents { } }); + @FunctionalInterface + public interface ControllerConnected { + void onControllerConnected(Controller controller, boolean hotplugged, boolean newController); + } + + @FunctionalInterface + public interface ControllerDisconnected { + void onControllerDisconnected(Controller controller); + } + @FunctionalInterface public interface InputModeChanged { void onInputModeChanged(InputMode mode); diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index 2f59ff7..eea8c36 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -2,7 +2,6 @@ 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; @@ -96,10 +95,13 @@ public class ControlifyConfig { JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data - for (var controller : ControllerManager.getConnectedControllers()) { - // `add` replaces if already existing - newControllerData.add(controller.uid(), generateControllerConfig(controller)); - } + controlify.getControllerManager().ifPresent(controllerManager -> { + for (Controller controller : controllerManager.getConnectedControllers()) { + // `add` replaces if already existing + newControllerData.add(controller.uid(), generateControllerConfig(controller)); + } + }); + controllerData = newControllerData; config.addProperty("current_controller", currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null)); @@ -137,8 +139,10 @@ public class ControlifyConfig { JsonObject controllers = object.getAsJsonObject("controllers"); if (controllers != null) { this.controllerData = controllers; - for (var controller : ControllerManager.getConnectedControllers()) { - loadOrCreateControllerData(controller); + if (controlify.getControllerManager().isPresent()) { + for (var controller : controlify.getControllerManager().get().getConnectedControllers()) { + loadOrCreateControllerData(controller); + } } } else { setDirty(); diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index 2ee16e2..8f204c6 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -29,7 +29,7 @@ public class GlobalSettings { public boolean allowServerRumble = true; public boolean uiSounds = false; public boolean notifyLowBattery = true; - public boolean delegateSetup = false; + public boolean quietMode = false; public float ingameButtonGuideScale = 1f; public Set seenServers = new HashSet<>(); diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index 0853d72..53abc61 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -4,7 +4,6 @@ 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.ControllerManager; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.hid.ControllerHIDService; import dev.isxander.controlify.rumble.RumbleCapable; @@ -62,7 +61,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/gamepad/GamepadController.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java index 96ac62f..e2cb943 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -73,6 +73,11 @@ public class GamepadController extends AbstractController joystickUids, String frien } public boolean canBeUsed() { - List> joysticks = ControllerManager.getConnectedControllers().stream().filter(c -> joystickUids.contains(c.uid())).toList(); + List> joysticks = Controlify.instance().getControllerManager().orElseThrow().getConnectedControllers().stream().filter(c -> joystickUids.contains(c.uid())).toList(); if (joysticks.size() != joystickUids().size()) { return false; // not all controllers are connected } @@ -26,13 +26,13 @@ public record CompoundJoystickInfo(Collection joystickUids, String frien } public boolean isLoaded() { - return ControllerManager.isControllerConnected(createUID(joystickUids)); + return Controlify.instance().getControllerManager().orElseThrow().isControllerConnected(createUID(joystickUids)); } public Optional attemptCreate() { if (!canBeUsed()) return Optional.empty(); - List joystickIDs = ControllerManager.getConnectedControllers().stream() + List joystickIDs = Controlify.instance().getControllerManager().orElseThrow().getConnectedControllers().stream() .filter(c -> joystickUids.contains(c.uid())) .map(Controller::joystickId) .toList(); diff --git a/src/main/java/dev/isxander/controlify/ControllerManager.java b/src/main/java/dev/isxander/controlify/controllermanager/AbstractControllerManager.java similarity index 52% rename from src/main/java/dev/isxander/controlify/ControllerManager.java rename to src/main/java/dev/isxander/controlify/controllermanager/AbstractControllerManager.java index 467a283..e688ea7 100644 --- a/src/main/java/dev/isxander/controlify/ControllerManager.java +++ b/src/main/java/dev/isxander/controlify/controllermanager/AbstractControllerManager.java @@ -1,33 +1,46 @@ -package dev.isxander.controlify; +package dev.isxander.controlify.controllermanager; import com.google.common.collect.ImmutableList; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.gamepad.GamepadController; -import dev.isxander.controlify.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.hid.ControllerHIDService; import dev.isxander.controlify.hid.HIDDevice; +import dev.isxander.controlify.utils.ControllerUtils; import dev.isxander.controlify.utils.DebugLog; import dev.isxander.controlify.utils.Log; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; -import org.hid4java.HidDevice; -import org.lwjgl.glfw.GLFW; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.resources.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -public final class ControllerManager { - private ControllerManager() { +import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError; + +public abstract class AbstractControllerManager implements ControllerManager { + protected final Controlify controlify; + protected final Minecraft minecraft; + private final Map> CONTROLLERS = new HashMap<>(); + + public AbstractControllerManager() { + this.controlify = Controlify.instance(); + this.minecraft = Minecraft.getInstance(); + + minecraft.getResourceManager() + .getResource(Controlify.id("controllers/gamecontrollerdb.txt")) + .ifPresent(this::loadGamepadMappings); } - private final static Map> CONTROLLERS = new HashMap<>(); - - public static Optional> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { + public Optional> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { try { Optional uid = hidInfo.createControllerUID(); if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { @@ -39,7 +52,7 @@ public final class ControllerManager { return Optional.empty(); } - if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) { + if (this.isControllerGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) { GamepadController controller = new GamepadController(joystickId, hidInfo); CONTROLLERS.put(controller.uid(), controller); checkCompoundJoysticks(); @@ -57,19 +70,52 @@ public final class ControllerManager { category.setDetail("Controller identification", hidInfo.type()); category.setDetail("HID path", hidInfo.hidDevice().map(HIDDevice::path).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")); + category.setDetail("GLFW name", Optional.ofNullable(getControllerSystemName(joystickId)).orElse("N/A")); throw new ReportedException(crashReport); } } - public static void disconnect(Controller controller) { + @Override + public void tick(boolean outOfFocus) { + for (var controller : this.getConnectedControllers()) { + if (!outOfFocus) + wrapControllerError(controller::updateState, "Updating controller state", controller); + else + wrapControllerError(controller::clearState, "Clearing controller state", controller); + ControlifyEvents.CONTROLLER_STATE_UPDATE.invoker().onControllerStateUpdate(controller); + } + } + + @Override + public Optional> getController(int joystickId) { + return CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == joystickId).findAny(); + } + + protected void onControllerConnected(Controller controller, boolean hotplug) { + Log.LOGGER.info("Controller connected: {}", ControllerUtils.createControllerString(controller)); + + boolean newController = controlify.config().loadOrCreateControllerData(controller); + + ControlifyEvents.CONTROLLER_CONNECTED.invoker().onControllerConnected(controller, hotplug, newController); + } + + protected void onControllerRemoved(Controller controller) { + Log.LOGGER.info("Controller disconnected: {}", ControllerUtils.createControllerString(controller)); + + controller.hidInfo().ifPresent(controlify.controllerHIDService()::unconsumeController); + removeController(controller); + + ControlifyEvents.CONTROLLER_DISCONNECTED.invoker().onControllerDisconnected(controller); + } + + protected void removeController(Controller controller) { controller.close(); CONTROLLERS.remove(controller.uid(), controller); checkCompoundJoysticks(); } - public static void disconnect(String uid) { + protected void removeController(String uid) { Controller prev = CONTROLLERS.remove(uid); if (prev != null) { prev.close(); @@ -78,20 +124,31 @@ public final class ControllerManager { checkCompoundJoysticks(); } - public static List> getConnectedControllers() { + @Override + public List> getConnectedControllers() { return ImmutableList.copyOf(CONTROLLERS.values()); } - public static boolean isControllerConnected(String uid) { + @Override + public boolean isControllerConnected(String uid) { return CONTROLLERS.containsKey(uid); } - private static void checkCompoundJoysticks() { + @Override + public void close() { + CONTROLLERS.values().forEach(Controller::close); + } + + protected abstract void loadGamepadMappings(Resource resource); + + protected abstract String getControllerSystemName(int joystickId); + + private void checkCompoundJoysticks() { Controlify.instance().config().getCompoundJoysticks().values().forEach(info -> { try { if (info.isLoaded() && !info.canBeUsed()) { Log.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers."); - disconnect(info.type().mappingId()); + removeController(info.type().mappingId()); } if (!info.isLoaded() && info.canBeUsed()) { diff --git a/src/main/java/dev/isxander/controlify/controllermanager/ControllerManager.java b/src/main/java/dev/isxander/controlify/controllermanager/ControllerManager.java new file mode 100644 index 0000000..8700b08 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controllermanager/ControllerManager.java @@ -0,0 +1,24 @@ +package dev.isxander.controlify.controllermanager; + +import dev.isxander.controlify.controller.Controller; + +import java.util.List; +import java.util.Optional; + +public interface ControllerManager { + void discoverControllers(); + + void tick(boolean outOfFocus); + + boolean probeConnectedControllers(); + + List> getConnectedControllers(); + + Optional> getController(int jid); + + boolean isControllerConnected(String uid); + + boolean isControllerGamepad(int jid); + + void close(); +} diff --git a/src/main/java/dev/isxander/controlify/controllermanager/GLFWControllerManager.java b/src/main/java/dev/isxander/controlify/controllermanager/GLFWControllerManager.java new file mode 100644 index 0000000..bc897fd --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controllermanager/GLFWControllerManager.java @@ -0,0 +1,96 @@ +package dev.isxander.controlify.controllermanager; + +import com.google.common.io.ByteStreams; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.utils.Log; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.resources.Resource; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.system.MemoryUtil; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.stream.IntStream; + +public class GLFWControllerManager extends AbstractControllerManager { + private final Minecraft minecraft; + + public GLFWControllerManager() { + this.minecraft = Minecraft.getInstance(); + + minecraft.getResourceManager() + .getResource(Controlify.id("controllers/gamecontrollerdb.txt")) + .ifPresent(this::loadGamepadMappings); + + this.setupCallbacks(); + } + + private void setupCallbacks() { + GLFW.glfwSetJoystickCallback((jid, event) -> { + try { + if (event == GLFW.GLFW_CONNECTED) { + createOrGet(jid, controlify.controllerHIDService().fetchType(jid)) + .ifPresent(controller -> onControllerConnected(controller, true)); + } else if (event == GLFW.GLFW_DISCONNECTED) { + getController(jid).ifPresent(this::onControllerRemoved); + } + } catch (Throwable e) { + Log.LOGGER.error("Failed to handle controller connect/disconnect event", e); + } + }); + } + + @Override + public void discoverControllers() { + for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) { + if (!GLFW.glfwJoystickPresent(i)) + continue; + + Optional> controllerOpt = createOrGet(i, controlify.controllerHIDService().fetchType(i)); + controllerOpt.ifPresent(controller -> onControllerConnected(controller, false)); + } + } + + @Override + public boolean probeConnectedControllers() { + return areControllersConnected(); + } + + @Override + public void tick(boolean outOfFocus) { + + } + + @Override + protected void loadGamepadMappings(Resource resource) { + Log.LOGGER.debug("Loading gamepad mappings..."); + + try (InputStream is = resource.open()) { + byte[] bytes = ByteStreams.toByteArray(is); + ByteBuffer buffer = MemoryUtil.memASCIISafe(new String(bytes)); + + if (!GLFW.glfwUpdateGamepadMappings(buffer)) { + Log.LOGGER.error("Failed to load gamepad mappings: {}", GLFW.glfwGetError(null)); + } + } catch (Throwable e) { + Log.LOGGER.error("Failed to load gamepad mappings: {}", e.getMessage()); + } + } + + @Override + public boolean isControllerGamepad(int jid) { + return GLFW.glfwJoystickIsGamepad(jid); + } + + @Override + protected String getControllerSystemName(int joystickId) { + return isControllerGamepad(joystickId) ? GLFW.glfwGetGamepadName(joystickId) : GLFW.glfwGetJoystickName(joystickId); + } + + public static boolean areControllersConnected() { + return IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1) + .anyMatch(GLFW::glfwJoystickPresent); + } +} diff --git a/src/main/java/dev/isxander/controlify/controllermanager/SDLControllerManager.java b/src/main/java/dev/isxander/controlify/controllermanager/SDLControllerManager.java new file mode 100644 index 0000000..9e0d355 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controllermanager/SDLControllerManager.java @@ -0,0 +1,130 @@ +package dev.isxander.controlify.controllermanager; + +import com.google.common.io.ByteStreams; +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.driver.SDL2NativesManager; +import dev.isxander.controlify.utils.Log; +import io.github.libsdl4j.api.event.SDL_Event; +import io.github.libsdl4j.api.event.SDL_EventFilter; +import io.github.libsdl4j.api.rwops.SDL_RWops; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.resources.Resource; +import org.apache.commons.lang3.Validate; + +import java.io.InputStream; +import java.util.Optional; + +import static io.github.libsdl4j.api.error.SdlError.SDL_GetError; +import static io.github.libsdl4j.api.event.SDL_EventType.*; +import static io.github.libsdl4j.api.event.SdlEvents.*; +import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.*; +import static io.github.libsdl4j.api.joystick.SdlJoystick.*; +import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem; + +public class SDLControllerManager extends AbstractControllerManager { + private final Controlify controlify; + private final Minecraft minecraft; + + private final SDL_Event event = new SDL_Event(); + + // must keep a reference to prevent GC from collecting it and the callback failing + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final EventFilter eventFilter; + + public SDLControllerManager() { + Validate.isTrue(SDL2NativesManager.isLoaded(), "SDL2 natives must be loaded before creating SDLControllerManager"); + + this.controlify = Controlify.instance(); + this.minecraft = Minecraft.getInstance(); + + SDL_SetEventFilter(eventFilter = new EventFilter(), Pointer.NULL); + } + + @Override + public void tick(boolean outOfFocus) { + while (SDL_PollEvent(event) == 1) { + switch (event.type) { + case SDL_JOYDEVICEADDED -> { + int jid = event.jdevice.which; + Optional> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid)); + controllerOpt.ifPresent(controller -> onControllerConnected(controller, true)); + } + case SDL_CONTROLLERDEVICEADDED -> { + int jid = event.cdevice.which; + Optional> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid)); + controllerOpt.ifPresent(controller -> onControllerConnected(controller, true)); + } + + case SDL_JOYDEVICEREMOVED -> { + int jid = event.jdevice.which; + getController(jid).ifPresent(this::onControllerRemoved); + } + case SDL_CONTROLLERDEVICEREMOVED -> { + int jid = event.cdevice.which; + getController(jid).ifPresent(this::onControllerRemoved); + } + } + } + } + + @Override + public void discoverControllers() { + for (int i = 0; i < SDL_NumJoysticks(); i++) { + Optional> controllerOpt = createOrGet(i, controlify.controllerHIDService().fetchType(i)); + controllerOpt.ifPresent(controller -> onControllerConnected(controller, false)); + } + } + + @Override + public boolean probeConnectedControllers() { + return SDL_NumJoysticks() > 0; + } + + @Override + public boolean isControllerGamepad(int jid) { + return SDL_IsGameController(jid); + } + + @Override + protected String getControllerSystemName(int joystickId) { + return isControllerGamepad(joystickId) ? SDL_GameControllerNameForIndex(joystickId) : SDL_JoystickNameForIndex(joystickId); + } + + @Override + protected void loadGamepadMappings(Resource resource) { + Log.LOGGER.debug("Loading gamepad mappings..."); + + try (InputStream is = resource.open()) { + byte[] bytes = ByteStreams.toByteArray(is); + + try (Memory memory = new Memory(bytes.length)) { + memory.write(0, bytes, 0, bytes.length); + SDL_RWops rw = SDL_RWFromConstMem(memory, bytes.length); + int count = SDL_GameControllerAddMappingsFromRW(rw, 1); + if (count < 1) { + Log.LOGGER.error("Failed to load gamepad mappings: {}", SDL_GetError()); + } + } + } catch (Throwable e) { + Log.LOGGER.error("Failed to load gamepad mappings", e); + } + } + + private static class EventFilter implements SDL_EventFilter { + @Override + public int filterEvent(Pointer userdata, SDL_Event event) { + switch (event.type) { + case SDL_JOYDEVICEADDED: + case SDL_JOYDEVICEREMOVED: + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + return 1; + default: + return 0; + } + } + } +} diff --git a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java index ea936c4..a78f5b0 100644 --- a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java +++ b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java @@ -23,7 +23,7 @@ public class DebugProperties { /** Print what drivers are being used */ public static final boolean PRINT_DRIVER = boolProp("controlify.debug.print_driver", true, true); /** Print the state of the left and right triggers on gamepads */ - public static final boolean PRINT_TRIGGER_STATE = boolProp("controlify.debug.print_trigger_state", false, false); + public static final boolean PRINT_GAMEPAD_STATE = boolProp("controlify.debug.print_gamepad_state", false, true); /** Use experimental anti-snapback */ public static final boolean USE_SNAPBACK = boolProp("controlify.debug.use_snapback", false, false); diff --git a/src/main/java/dev/isxander/controlify/driver/SDL2NativesManager.java b/src/main/java/dev/isxander/controlify/driver/SDL2NativesManager.java index 10af2fe..1baa03b 100644 --- a/src/main/java/dev/isxander/controlify/driver/SDL2NativesManager.java +++ b/src/main/java/dev/isxander/controlify/driver/SDL2NativesManager.java @@ -97,7 +97,7 @@ public class SDL2NativesManager { SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1"); // initialise SDL with just joystick and gamecontroller subsystems - if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) { + if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS) != 0) { Log.LOGGER.error("Failed to initialise SDL2: " + SDL_GetError()); throw new RuntimeException("Failed to initialise SDL2: " + SDL_GetError()); } diff --git a/src/main/java/dev/isxander/controlify/driver/gamepad/SDL2GamepadDriver.java b/src/main/java/dev/isxander/controlify/driver/gamepad/SDL2GamepadDriver.java index 48b627a..092563c 100644 --- a/src/main/java/dev/isxander/controlify/driver/gamepad/SDL2GamepadDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/gamepad/SDL2GamepadDriver.java @@ -7,6 +7,7 @@ import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.driver.*; import dev.isxander.controlify.utils.Log; import io.github.libsdl4j.api.gamecontroller.SDL_GameController; +import io.github.libsdl4j.api.joystick.SdlJoystick; import net.minecraft.util.Mth; import static io.github.libsdl4j.api.error.SdlError.*; @@ -26,6 +27,9 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R public SDL2GamepadDriver(int jid) { this.ptrGamepad = SDL_GameControllerOpen(jid); + if (ptrGamepad == null) + throw new IllegalStateException("Could not open gamepad: " + SDL_GetError()); + this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString(); this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO); this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCalibrationScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCalibrationScreen.java index fdd0560..4782a8a 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCalibrationScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCalibrationScreen.java @@ -1,12 +1,12 @@ package dev.isxander.controlify.gui.screen; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping; +import dev.isxander.controlify.controllermanager.ControllerManager; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -33,6 +33,8 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS private static final ResourceLocation GREEN_BACK_BAR = new ResourceLocation("boss_bar/green_background"); private static final ResourceLocation GREEN_FRONT_BAR = new ResourceLocation("boss_bar/green_progress"); + protected final Controlify controlify; + protected final ControllerManager controllerManager; protected final Controller controller; private final Supplier parent; @@ -52,6 +54,8 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS public ControllerCalibrationScreen(Controller controller, Supplier parent) { super(Component.translatable("controlify.calibration.title")); + this.controlify = Controlify.instance(); + this.controllerManager = controlify.getControllerManager().orElseThrow(); this.controller = controller; this.parent = parent; this.axisData = new double[controller.axisCount() * CALIBRATION_TIME]; @@ -129,7 +133,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS @Override public void tick() { - if (!ControllerManager.isControllerConnected(controller.uid())) { + if (!controllerManager.isControllerConnected(controller.uid())) { onClose(); return; } @@ -161,7 +165,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS controller.config().delayedCalibration = false; // no need to save because of setCurrentController - Controlify.instance().setCurrentController(controller); + Controlify.instance().setCurrentController(controller, true); } } @@ -231,7 +235,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS if (!controller.config().deadzonesCalibrated) { controller.config().delayedCalibration = true; Controlify.instance().config().setDirty(); - Controlify.instance().setCurrentController(null); + Controlify.instance().setCurrentController(null, true); } onClose(); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java index 65c6c91..ecc59d2 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java @@ -2,17 +2,17 @@ package dev.isxander.controlify.gui.screen; import com.google.common.collect.ImmutableList; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.api.buttonguide.ButtonGuideApi; import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate; import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition; import dev.isxander.controlify.controller.Controller; -import dev.isxander.controlify.driver.SDL2NativesManager; +import dev.isxander.controlify.controllermanager.ControllerManager; import dev.isxander.controlify.gui.components.FakePositionPlainTextButton; import dev.isxander.controlify.screenop.ScreenControllerEventListener; import dev.isxander.controlify.utils.Animator; import net.minecraft.ChatFormatting; import net.minecraft.Util; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -47,24 +47,27 @@ public class ControllerCarouselScreen extends Screen implements ScreenController private int carouselIndex; private Animator.AnimationInstance carouselAnimation = null; + private final Controlify controlify; + private final ControllerManager controllerManager; + private Button globalSettingsButton, doneButton; private ControllerCarouselScreen(Screen parent) { super(Component.translatable("controlify.gui.carousel.title")); this.parent = parent; - this.carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0); + + this.controlify = Controlify.instance(); + this.controllerManager = controlify.getControllerManager().orElseThrow(); + + this.carouselIndex = controlify.getCurrentController().map(c -> controllerManager.getConnectedControllers().indexOf(c)).orElse(0); } - public static Screen createConfigScreen(Screen parent) { + public static void openConfigScreen(Screen parent) { var controlify = Controlify.instance(); - if (controlify.config().globalSettings().delegateSetup) { - controlify.discoverControllers(); - controlify.config().globalSettings().delegateSetup = false; - controlify.config().save(); - } - - return new ControllerCarouselScreen(parent); + controlify.finishControlifyInit().whenComplete((v, th) -> { + Minecraft.getInstance().setScreen(new ControllerCarouselScreen(parent)); + }); } @Override @@ -109,7 +112,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController prevSelectedController = null; } - carouselEntries = ControllerManager.getConnectedControllers().stream() + carouselEntries = controllerManager.getConnectedControllers().stream() .map(c -> new CarouselEntry(c, this.width / 3, this.height - 66)) .peek(this::addRenderableWidget) .toList(); @@ -118,7 +121,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController .findFirst() .map(carouselEntries::indexOf) .orElse(Controlify.instance().getCurrentController() - .map(c -> ControllerManager.getConnectedControllers().indexOf(c)) + .map(c -> controllerManager.getConnectedControllers().indexOf(c)) .orElse(0) ); if (!carouselEntries.isEmpty()) @@ -220,7 +223,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController this.hasNickname = this.controller.config().customName != null; this.settingsButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.settings"), btn -> minecraft.setScreen(ControllerConfigScreenFactory.generateConfigScreen(ControllerCarouselScreen.this, controller))).width((getWidth() - 2) / 2 - 2).build(); - this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller)).width(settingsButton.getWidth()).build(); + this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller, true)).width(settingsButton.getWidth()).build(); this.children = ImmutableList.of(settingsButton, useControllerButton); this.prevUse = isCurrentlyUsed(); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ModConfigOpenerScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ModConfigOpenerScreen.java index dd7d57b..831700e 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ModConfigOpenerScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ModConfigOpenerScreen.java @@ -1,6 +1,5 @@ package dev.isxander.controlify.gui.screen; -import dev.isxander.controlify.Controlify; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; @@ -21,10 +20,7 @@ public class ModConfigOpenerScreen extends Screen { Minecraft minecraft = Minecraft.getInstance(); this.init(minecraft, minecraft.getWindow().getGuiScaledWidth(), minecraft.getWindow().getGuiScaledHeight()); - Controlify.instance().askNatives() - .whenComplete((result, error) -> - minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen)) - ); + ControllerCarouselScreen.openConfigScreen(lastScreen); } @Override 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 4a4dde3..2ab7b9c 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -2,9 +2,8 @@ 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.api.ControlifyApi; -import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controllermanager.ControllerManager; import dev.isxander.controlify.utils.Animator; import dev.isxander.controlify.utils.MouseMinecraftCallNotifier; import net.minecraft.client.Minecraft; @@ -72,7 +71,7 @@ public abstract class MinecraftMixin { @Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/telemetry/ClientTelemetryManager;close()V")) private void onMinecraftClose(CallbackInfo ci) { - ControllerManager.getConnectedControllers().forEach(Controller::close); + Controlify.instance().getControllerManager().ifPresent(ControllerManager::close); } @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;render(FJZ)V")) diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/chatkbheight/ChatComponentMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/chatkbheight/ChatComponentMixin.java index d827d38..77963cd 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/chatkbheight/ChatComponentMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/chatkbheight/ChatComponentMixin.java @@ -24,10 +24,12 @@ public class ChatComponentMixin { if (!(minecraft.screen instanceof ChatScreen)) return; - Controller controller = Controlify.instance().currentController(); - graphics.pose().pushPose(); - if (controller.config().chatKeyboardHeight == 0) return; - graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0); + Controlify.instance().getCurrentController().ifPresent(controller -> { + graphics.pose().pushPose(); + if (controller.config().chatKeyboardHeight == 0) return; + graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0); + }); + } @Inject(method = "render", at = @At("TAIL")) diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java index 30e1f0c..e96d323 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java @@ -37,9 +37,6 @@ public class ControlsScreenMixin extends OptionsSubScreen { @Unique private void openControllerSettings() { - Controlify.instance().askNatives() - .whenComplete((result, error) -> - minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen)) - ); + ControllerCarouselScreen.openConfigScreen(this); } } diff --git a/src/main/java/dev/isxander/controlify/utils/ControllerUtils.java b/src/main/java/dev/isxander/controlify/utils/ControllerUtils.java index caef878..81f82d0 100644 --- a/src/main/java/dev/isxander/controlify/utils/ControllerUtils.java +++ b/src/main/java/dev/isxander/controlify/utils/ControllerUtils.java @@ -5,6 +5,9 @@ import dev.isxander.controlify.controller.ControllerType; import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.hid.ControllerHIDService; import dev.isxander.controlify.hid.HIDDevice; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; import net.minecraft.util.Mth; import org.joml.Vector2f; @@ -29,6 +32,19 @@ public class ControllerUtils { ); } + 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 static float deadzone(float value, float deadzone) { return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone); } diff --git a/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java b/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java index 5acf108..fbf35a6 100644 --- a/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java +++ b/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java @@ -1,9 +1,9 @@ package dev.isxander.controlify.wireless; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.BatteryLevel; import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controllermanager.ControllerManager; import dev.isxander.controlify.utils.ToastUtils; import net.minecraft.network.chat.Component; @@ -24,7 +24,11 @@ public class LowBatteryNotifier { if (!Controlify.instance().config().globalSettings().notifyLowBattery) return; - for (Controller controller : ControllerManager.getConnectedControllers()) { + ControllerManager controllerManager = Controlify.instance().getControllerManager().orElse(null); + if (controllerManager == null) + return; + + for (Controller controller : controllerManager.getConnectedControllers()) { BatteryLevel batteryLevel = controller.batteryLevel(); if (batteryLevel == BatteryLevel.UNKNOWN) { continue; diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_down.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/a_button.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_down.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/a_button.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_right.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/b_button.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_right.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/b_button.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_select_button.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/back.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_select_button.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/back.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_down.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_down.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_down.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_down.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_left.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_left.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_left.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_left.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_right.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_right.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_right.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_right.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_up.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_up.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_dpad_up.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/dpad_up.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_bumper_left.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_bumper.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_bumper_left.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_bumper.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_stick_left.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_stick_down.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_stick_left.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_stick_down.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_left_trigger.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_trigger.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_left_trigger.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/left_trigger.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_bumper_right.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_bumper.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_bumper_right.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_bumper.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_stick_right.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_stick_down.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_stick_right.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_stick_down.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_right_trigger.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_trigger.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_right_trigger.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/right_trigger.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_start_button.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/start.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_start_button.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/start.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_left.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/x_button.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_left.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/x_button.png diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_up.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/y_button.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/gamepad/switch/switch_face_button_up.png rename to src/main/resources/assets/controlify/textures/gui/gamepad/switch/y_button.png diff --git a/src/testmod/java/dev/isxander/controlify/test/FakeController.java b/src/testmod/java/dev/isxander/controlify/test/FakeController.java index bcdcb2b..0ae9d36 100644 --- a/src/testmod/java/dev/isxander/controlify/test/FakeController.java +++ b/src/testmod/java/dev/isxander/controlify/test/FakeController.java @@ -194,11 +194,11 @@ public class FakeController implements JoystickController { } public void use() { - Controlify.instance().setCurrentController(this); + Controlify.instance().setCurrentController(this, true); } public void finish() { - Controlify.instance().setCurrentController(null); + Controlify.instance().setCurrentController(null, true); } @Override