From 09628defc4c772fb1be16c873975bd13e8bad42b Mon Sep 17 00:00:00 2001 From: isXander Date: Thu, 2 Feb 2023 21:36:44 +0000 Subject: [PATCH] virtual mouse + singleplayer screen compat + 22w05a --- gradle/libs.versions.toml | 6 +- .../dev/isxander/controlify/Controlify.java | 85 +++++-- .../bindings/ControllerBindings.java | 11 +- .../compatibility/screen/ScreenProcessor.java | 49 +++- .../screen/ScreenProcessorProvider.java | 4 +- .../screen/component/ComponentProcessor.java | 6 +- .../vanilla/SelectWorldScreenProcessor.java | 25 ++ .../SliderComponentProcessor.java | 10 +- .../WorldListEntryComponentProcessor.java | 21 ++ .../controlify/config/ControlifyConfig.java | 57 +++-- .../controlify/config/GlobalSettings.java | 13 ++ .../controlify/config/gui/YACLHelper.java | 45 +++- .../controlify/controller/Controller.java | 9 +- .../controller/ControllerConfig.java | 24 +- .../controlify/event/ControlifyEvents.java | 11 + .../vanilla/AbstractSelectionListMixin.java | 2 +- .../vanilla/AbstractSliderButtonMixin.java | 2 +- .../compat/screen/vanilla/ScreenAccessor.java | 3 + .../compat/screen/vanilla/ScreenMixin.java | 4 +- .../vanilla/SelectWorldScreenAccessor.java | 16 ++ .../vanilla/SelectWorldScreenMixin.java | 17 ++ .../vanilla/WorldSelectionListEntryMixin.java | 17 ++ .../core/ClientPacketListenerMixin.java | 10 +- .../mixins/core/KeyboardHandlerMixin.java | 13 +- .../mixins/core/MinecraftMixin.java | 4 +- .../mixins/core/MouseHandlerMixin.java | 23 +- .../virtualmouse/GameRendererMixin.java | 18 ++ .../virtualmouse/KeyboardHandlerAccessor.java | 11 + .../feature/virtualmouse/MinecraftMixin.java | 22 ++ .../virtualmouse/MouseHandlerAccessor.java | 17 ++ .../virtualmouse/VirtualMouseHandler.java | 218 ++++++++++++++++++ .../assets/controlify/lang/en_us.json | 26 ++- .../gui/{pointer.png => virtual_mouse.png} | Bin src/main/resources/controlify.mixins.json | 9 +- 34 files changed, 697 insertions(+), 111 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/compatibility/vanilla/SelectWorldScreenProcessor.java rename src/main/java/dev/isxander/controlify/compatibility/{screen/component => vanilla}/SliderComponentProcessor.java (83%) create mode 100644 src/main/java/dev/isxander/controlify/compatibility/vanilla/WorldListEntryComponentProcessor.java create mode 100644 src/main/java/dev/isxander/controlify/config/GlobalSettings.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/WorldSelectionListEntryMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/GameRendererMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/KeyboardHandlerAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MinecraftMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java rename src/main/resources/assets/controlify/textures/gui/{pointer.png => virtual_mouse.png} (100%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e04dfa8..9fd9628 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,10 +7,10 @@ github_release = "2.+" machete = "1.+" grgit = "5.0.+" -minecraft = "23w04a" -quilt_mappings = "10" +minecraft = "23w05a" +quilt_mappings = "1" fabric_loader = "0.14.13" -fabric_api = "0.73.1+1.19.4" +fabric_api = "0.73.3+1.19.4" mixin_extras = "0.2.0-beta.1" yet_another_config_lib = "2.2.0+update.1.19.4-SNAPSHOT" mod_menu = "6.0.0-beta.1" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 642bba3..c92193c 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -7,8 +7,12 @@ import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.event.ControlifyEvents; import dev.isxander.controlify.ingame.InGameInputHandler; +import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor; +import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.network.chat.Component; import org.lwjgl.glfw.GLFW; import org.slf4j.Logger; @@ -18,20 +22,24 @@ public class Controlify { private Controller currentController; private InGameInputHandler inGameInputHandler; + private VirtualMouseHandler virtualMouseHandler; private InputMode currentInputMode; + private final ControlifyConfig config = new ControlifyConfig(); + public void onInitializeInput() { + Minecraft minecraft = Minecraft.getInstance(); + // find already connected controllers for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) { if (GLFW.glfwJoystickPresent(i)) { setCurrentController(Controller.byId(i)); LOGGER.info("Controller found: " + currentController.name()); - this.setCurrentInputMode(InputMode.CONTROLLER); } } // load after initial controller discovery - ControlifyConfig.load(); + config().load(); // listen for new controllers GLFW.glfwSetJoystickCallback((jid, event) -> { @@ -40,16 +48,32 @@ public class Controlify { LOGGER.info("Controller connected: " + currentController.name()); this.setCurrentInputMode(InputMode.CONTROLLER); - ControlifyConfig.load(); // load config again if a configuration already exists for this controller - ControlifyConfig.save(); // save config if it doesn't exist + config().load(); // load config again if a configuration already exists for this controller + config().save(); // save config if it doesn't exist + + minecraft.getToasts().addToast(SystemToast.multiline( + minecraft, + SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, + Component.translatable("controlify.toast.controller_connected.title"), + Component.translatable("controlify.toast.controller_connected.description") + )); } else if (event == GLFW.GLFW_DISCONNECTED) { var controller = Controller.CONTROLLERS.remove(jid); setCurrentController(Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null)); LOGGER.info("Controller disconnected: " + controller.name()); this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); + + minecraft.getToasts().addToast(SystemToast.multiline( + minecraft, + SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, + Component.translatable("controlify.toast.controller_disconnected.title"), + Component.translatable("controlify.toast.controller_disconnected.description", controller.name()) + )); } }); + this.virtualMouseHandler = new VirtualMouseHandler(); + ClientTickEvents.START_CLIENT_TICK.register(this::tick); } @@ -69,39 +93,74 @@ public class Controlify { } if (client.screen != null) { - ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + if (!this.virtualMouseHandler().isVirtualMouseEnabled()) + ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); } else { - this.getInGameInputHandler().inputTick(); + this.inGameInputHandler().inputTick(); } + this.virtualMouseHandler().handleControllerInput(currentController); } - public Controller getCurrentController() { + public ControlifyConfig config() { + return config; + } + + public Controller currentController() { return currentController; } public void setCurrentController(Controller controller) { if (this.currentController == controller) return; - this.currentController = controller; - this.inGameInputHandler = new InGameInputHandler(controller); + + this.inGameInputHandler = new InGameInputHandler(this.currentController != null ? controller : Controller.DUMMY); } - public InGameInputHandler getInGameInputHandler() { + public InGameInputHandler inGameInputHandler() { return inGameInputHandler; } - public InputMode getCurrentInputMode() { + public VirtualMouseHandler virtualMouseHandler() { + return virtualMouseHandler; + } + + public InputMode currentInputMode() { return currentInputMode; } public void setCurrentInputMode(InputMode currentInputMode) { if (this.currentInputMode == currentInputMode) return; - this.currentInputMode = currentInputMode; + + var minecraft = Minecraft.getInstance(); + hideMouse(currentInputMode == InputMode.CONTROLLER); + if (minecraft.screen != null) { + ScreenProcessorProvider.provide(minecraft.screen).onInputModeChanged(currentInputMode); + } + ControlifyEvents.INPUT_MODE_CHANGED.invoker().onInputModeChanged(currentInputMode); } - public static Controlify getInstance() { + public void hideMouse(boolean hide) { + var minecraft = Minecraft.getInstance(); + GLFW.glfwSetInputMode( + minecraft.getWindow().getWindow(), + GLFW.GLFW_CURSOR, + hide + ? GLFW.GLFW_CURSOR_HIDDEN + : GLFW.GLFW_CURSOR_NORMAL + ); + if (minecraft.screen != null) { + var mouseHandlerAccessor = (MouseHandlerAccessor) minecraft.mouseHandler; + if (hide && !virtualMouseHandler().isVirtualMouseEnabled()) { + // stop mouse hovering over last element before hiding cursor but don't actually move it + // so when the user switches back to mouse it will be in the same place + mouseHandlerAccessor.invokeOnMove(minecraft.getWindow().getWindow(), 0, 0); + } + } + } + + public static Controlify instance() { if (instance == null) instance = new Controlify(); return instance; } diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index 205a0d9..a8b9c95 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -23,7 +23,8 @@ public class ControllerBindings { INVENTORY, CHANGE_PERSPECTIVE, OPEN_CHAT, - GUI_PRESS, GUI_BACK; + GUI_PRESS, GUI_BACK, + VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_MCLICK, VMOUSE_ESCAPE, VMOUSE_TOGGLE; private final Map registry = new LinkedHashMap<>(); @@ -44,7 +45,11 @@ public class ControllerBindings { register(OPEN_CHAT = new ControllerBinding(controller, Bind.DPAD_UP, new ResourceLocation("controlify", "open_chat"), options.keyChat)); register(GUI_PRESS = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "gui_press"), null)); register(GUI_BACK = new ControllerBinding(controller, Bind.B_BUTTON, new ResourceLocation("controlify", "gui_back"), null)); - + register(VMOUSE_LCLICK = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "vmouse_lclick"), null)); + register(VMOUSE_RCLICK = new ControllerBinding(controller, Bind.X_BUTTON, new ResourceLocation("controlify", "vmouse_rclick"), null)); + register(VMOUSE_MCLICK = new ControllerBinding(controller, Bind.Y_BUTTON, new ResourceLocation("controlify", "vmouse_mclick"), null)); + register(VMOUSE_ESCAPE = new ControllerBinding(controller, Bind.B_BUTTON, new ResourceLocation("controlify", "vmouse_escape"), null)); + register(VMOUSE_TOGGLE = new ControllerBinding(controller, Bind.BACK, new ResourceLocation("controlify", "vmouse_toggle"), null)); ControlifyEvents.CONTROLLER_BIND_REGISTRY.invoker().onRegisterControllerBinds(this, controller); @@ -82,7 +87,7 @@ public class ControllerBindings { } private void imitateVanillaClick(Controller controller) { - if (Controlify.getInstance().getCurrentInputMode() != InputMode.CONTROLLER) + if (Controlify.instance().currentInputMode() != InputMode.CONTROLLER) return; for (var binding : registry().values()) { diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java index 5502eef..129e4a9 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java @@ -2,12 +2,13 @@ package dev.isxander.controlify.compatibility.screen; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; import dev.isxander.controlify.compatibility.screen.component.CustomFocus; import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.event.ControlifyEvents; import dev.isxander.controlify.mixins.compat.screen.vanilla.ScreenAccessor; import net.minecraft.client.gui.ComponentPath; -import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenDirection; @@ -15,16 +16,14 @@ import net.minecraft.client.gui.screens.Screen; import org.lwjgl.glfw.GLFW; import java.util.*; -import java.util.concurrent.ArrayBlockingQueue; -public class ScreenProcessor { - private static final int REPEAT_DELAY = 3; - - public final Screen screen; +public class ScreenProcessor { + public final T screen; private int lastMoved = 0; - public ScreenProcessor(Screen screen) { + public ScreenProcessor(T screen) { this.screen = screen; + ControlifyEvents.VIRTUAL_MOUSE_TOGGLED.register(this::onVirtualMouseToggled); } public void onControllerUpdate(Controller controller) { @@ -32,6 +31,17 @@ public class ScreenProcessor { handleButtons(controller); } + public void onInputModeChanged(InputMode mode) { + switch (mode) { + case KEYBOARD_MOUSE -> ((ScreenAccessor) screen).invokeClearFocus(); + case CONTROLLER -> { + if (!Controlify.instance().virtualMouseHandler().isVirtualMouseEnabled()) { + setInitialFocus(); + } + } + } + } + protected void handleComponentNavigation(Controller controller) { var focusTree = getFocusTree(); while (!focusTree.isEmpty()) { @@ -42,7 +52,7 @@ public class ScreenProcessor { var accessor = (ScreenAccessor) screen; - boolean repeatEventAvailable = ++lastMoved >= REPEAT_DELAY; + boolean repeatEventAvailable = ++lastMoved >= controller.config().screenRepeatNavigationDelay; var axes = controller.state().axes(); var prevAxes = controller.prevState().axes(); @@ -93,8 +103,19 @@ public class ScreenProcessor { } public void onWidgetRebuild() { - // initial focus - if (screen.getFocused() == null && Controlify.getInstance().getCurrentInputMode() == InputMode.CONTROLLER) { + setInitialFocus(); + } + + public void onVirtualMouseToggled(boolean enabled) { + if (enabled) { + ((ScreenAccessor) screen).invokeClearFocus(); + } else { + setInitialFocus(); + } + } + + protected void setInitialFocus() { + if (screen.getFocused() == null && Controlify.instance().currentInputMode() == InputMode.CONTROLLER && !Controlify.instance().virtualMouseHandler().isVirtualMouseEnabled()) { var accessor = (ScreenAccessor) screen; ComponentPath path = screen.nextFocusPath(accessor.invokeCreateArrowEvent(ScreenDirection.DOWN)); if (path != null) @@ -102,6 +123,10 @@ public class ScreenProcessor { } } + public boolean forceVirtualMouse() { + return false; + } + protected Queue getFocusTree() { if (screen.getFocused() == null) return new ArrayDeque<>(); @@ -110,7 +135,9 @@ public class ScreenProcessor { tree.add(focused); while (focused instanceof CustomFocus customFocus) { focused = customFocus.getCustomFocus(); - tree.addFirst(focused); + + if (focused != null) + tree.addFirst(focused); } return tree; diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java index 0625c33..0549f08 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessorProvider.java @@ -3,9 +3,9 @@ package dev.isxander.controlify.compatibility.screen; import net.minecraft.client.gui.screens.Screen; public interface ScreenProcessorProvider { - ScreenProcessor screenProcessor(); + ScreenProcessor screenProcessor(); - static ScreenProcessor provide(Screen screen) { + static ScreenProcessor provide(Screen screen) { return ((ScreenProcessorProvider) screen).screenProcessor(); } } diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java index 236994b..6bf3f3c 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/component/ComponentProcessor.java @@ -10,14 +10,14 @@ import net.minecraft.client.gui.components.events.GuiEventListener; public interface ComponentProcessor { ComponentProcessor EMPTY = new ComponentProcessor(){}; - default boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { + default boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { return false; } - default boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + default boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { return false; } - default void onNavigateTo(ScreenProcessor screen, Controller controller) { + default void onNavigateTo(ScreenProcessor screen, Controller controller) { } } diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/SelectWorldScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/SelectWorldScreenProcessor.java new file mode 100644 index 0000000..b6690e4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/SelectWorldScreenProcessor.java @@ -0,0 +1,25 @@ +package dev.isxander.controlify.compatibility.vanilla; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.mixins.compat.screen.vanilla.SelectWorldScreenAccessor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; + +public class SelectWorldScreenProcessor extends ScreenProcessor { + public SelectWorldScreenProcessor(SelectWorldScreen screen) { + super(screen); + } + + @Override + protected void handleButtons(Controller controller) { + if (screen.getFocused() != null && screen.getFocused() instanceof Button) { + if (controller.bindings().GUI_BACK.justPressed()) { + screen.setFocused(((SelectWorldScreenAccessor) screen).getList()); + return; + } + } + + super.handleButtons(controller); + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/SliderComponentProcessor.java similarity index 83% rename from src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java rename to src/main/java/dev/isxander/controlify/compatibility/vanilla/SliderComponentProcessor.java index b4ee3c3..d0ba925 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/screen/component/SliderComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/SliderComponentProcessor.java @@ -1,6 +1,7 @@ -package dev.isxander.controlify.compatibility.screen.component; +package dev.isxander.controlify.compatibility.vanilla; import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; import dev.isxander.controlify.controller.Controller; import net.minecraft.client.gui.components.AbstractSliderButton; import org.lwjgl.glfw.GLFW; @@ -24,7 +25,7 @@ public class SliderComponentProcessor implements ComponentProcessor { } @Override - public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { + public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) { if (!this.canChangeValueGetter.get()) return false; var canSliderChange = ++lastSliderChange > SLIDER_CHANGE_DELAY; @@ -51,7 +52,7 @@ public class SliderComponentProcessor implements ComponentProcessor { } @Override - public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { if (!this.canChangeValueGetter.get()) return false; if (controller.bindings().GUI_BACK.justPressed()) { @@ -63,7 +64,8 @@ public class SliderComponentProcessor implements ComponentProcessor { } @Override - public void onNavigateTo(ScreenProcessor screen, Controller controller) { + public void onNavigateTo(ScreenProcessor screen, Controller controller) { + System.out.println("navigated!"); this.canChangeValueSetter.accept(false); } } diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/WorldListEntryComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/WorldListEntryComponentProcessor.java new file mode 100644 index 0000000..1dc4236 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/WorldListEntryComponentProcessor.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.compatibility.vanilla; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.mixins.compat.screen.vanilla.SelectWorldScreenAccessor; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; + +public class WorldListEntryComponentProcessor implements ComponentProcessor { + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (controller.bindings().GUI_PRESS.justPressed()) { + var selectWorldScreen = (SelectWorldScreen) screen.screen; + selectWorldScreen.setFocused(((SelectWorldScreenAccessor) selectWorldScreen).getSelectButton()); + + return true; + } + + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index 3a8a724..6546a5e 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.config; import com.google.gson.*; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerConfig; import net.fabricmc.loader.api.FabricLoader; @@ -19,20 +20,23 @@ public class ControlifyConfig { .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create(); - private static JsonObject config = new JsonObject(); + private JsonObject controllerData = new JsonObject(); + private GlobalSettings globalSettings = new GlobalSettings(); + + public void save() { + Controlify.LOGGER.info("Saving Controlify config..."); - public static void save() { try { - generateConfig(); - Files.deleteIfExists(CONFIG_PATH); - Files.writeString(CONFIG_PATH, GSON.toJson(config), StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING); + Files.writeString(CONFIG_PATH, GSON.toJson(generateConfig()), StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException e) { throw new IllegalStateException("Failed to save config!", e); } } - public static void load() { + public void load() { + Controlify.LOGGER.info("Loading Controlify config..."); + if (!Files.exists(CONFIG_PATH)) { save(); return; @@ -45,19 +49,26 @@ public class ControlifyConfig { } } - private static void generateConfig() { - JsonObject configCopy = config.deepCopy(); // we use the old config, so we don't lose disconnected controller data + private JsonObject generateConfig() { + JsonObject config = new JsonObject(); + + JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data for (var controller : Controller.CONTROLLERS.values()) { // `add` replaces if already existing // TODO: find a better way to identify controllers, GUID will report the same for multiple controllers of the same model - configCopy.add(controller.guid(), generateControllerConfig(controller)); + newControllerData.add(controller.guid(), generateControllerConfig(controller)); } - config = configCopy; + controllerData = newControllerData; + config.add("controllers", controllerData); + + config.add("global", GSON.toJsonTree(globalSettings)); + + return config; } - private static JsonObject generateControllerConfig(Controller controller) { + private JsonObject generateControllerConfig(Controller controller) { JsonObject object = new JsonObject(); object.add("config", GSON.toJsonTree(controller.config())); @@ -66,17 +77,27 @@ public class ControlifyConfig { return object; } - private static void applyConfig(JsonObject object) { - for (var controller : Controller.CONTROLLERS.values()) { - var settings = object.getAsJsonObject(controller.guid()); - if (settings != null) { - applyControllerConfig(controller, settings); + private void applyConfig(JsonObject object) { + globalSettings = GSON.fromJson(object.getAsJsonObject("global"), GlobalSettings.class); + if (globalSettings == null) globalSettings = new GlobalSettings(); + + JsonObject controllers = object.getAsJsonObject("controllers"); + if (controllers != null) { + for (var controller : Controller.CONTROLLERS.values()) { + var settings = controllers.getAsJsonObject(controller.guid()); + if (settings != null) { + applyControllerConfig(controller, settings); + } } } } - private static void applyControllerConfig(Controller controller, JsonObject object) { - controller.config().overwrite(GSON.fromJson(object.getAsJsonObject("config"), ControllerConfig.class)); + private void applyControllerConfig(Controller controller, JsonObject object) { + controller.setConfig(GSON.fromJson(object.getAsJsonObject("config"), ControllerConfig.class)); controller.bindings().fromJson(object.getAsJsonObject("bindings")); } + + public GlobalSettings globalSettings() { + return globalSettings; + } } diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java new file mode 100644 index 0000000..f40887d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -0,0 +1,13 @@ +package dev.isxander.controlify.config; + +import com.google.common.collect.Lists; + +import java.util.List; + +public class GlobalSettings { + public static final GlobalSettings DEFAULT = new GlobalSettings(); + + public List virtualMouseScreens = Lists.newArrayList( + + ); +} 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 435285f..b2027d7 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -1,23 +1,46 @@ package dev.isxander.controlify.config.gui; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.bindings.Bind; -import dev.isxander.controlify.config.ControlifyConfig; +import dev.isxander.controlify.config.GlobalSettings; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerConfig; -import dev.isxander.yacl.api.ConfigCategory; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionGroup; -import dev.isxander.yacl.api.YetAnotherConfigLib; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.controllers.cycling.CyclingListController; import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; +import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; +import dev.isxander.yacl.gui.controllers.string.StringController; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; public class YACLHelper { public static Screen generateConfigScreen(Screen parent) { + var controlify = Controlify.instance(); + var yacl = YetAnotherConfigLib.createBuilder() .title(Component.literal("Controlify")) - .save(ControlifyConfig::save); + .save(() -> controlify.config().save()); + + var globalCategory = ConfigCategory.createBuilder() + .name(Component.translatable("controlify.gui.category.global")) + .option(Option.createBuilder(Controller.class) + .name(Component.translatable("controlify.gui.current_controller")) + .tooltip(Component.translatable("controlify.gui.current_controller.tooltip")) + .binding(Controlify.instance().currentController(), () -> Controlify.instance().currentController(), v -> Controlify.instance().setCurrentController(v)) + .controller(opt -> new CyclingListController<>(opt, Controller.CONTROLLERS.values().stream().filter(Controller::connected).toList(), c -> Component.literal(c.name()))) + .instant(true) + .build()) + .option(ListOption.createBuilder(String.class) + .name(Component.translatable("controlify.gui.vmouse_screens")) + .tooltip(Component.translatable("controlify.gui.vmouse_screens.tooltip")) + .binding(GlobalSettings.DEFAULT.virtualMouseScreens, () -> controlify.config().globalSettings().virtualMouseScreens, v -> controlify.config().globalSettings().virtualMouseScreens = v) + .controller(StringController::new) + .initial(Language.getInstance().getOrDefault("controlify.gui.vmouse_screens.placeholder")) + .build()); + + yacl.category(globalCategory.build()); for (var controller : Controller.CONTROLLERS.values()) { var category = ConfigCategory.createBuilder(); @@ -42,19 +65,25 @@ public class YACLHelper { .binding(def.verticalLookSensitivity, () -> config.verticalLookSensitivity, v -> config.verticalLookSensitivity = v) .controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100)))) .build()) + .option(Option.createBuilder(int.class) + .name(Component.translatable("controlify.gui.screen_repeat_navi_delay")) + .tooltip(Component.translatable("controlify.gui.screen_repeat_navi_delay.tooltip")) + .binding(def.screenRepeatNavigationDelay, () -> config.screenRepeatNavigationDelay, v -> config.screenRepeatNavigationDelay = v) + .controller(opt -> new IntegerSliderController(opt, 1, 20, 1, v -> Component.translatable("controlify.gui.format.ticks", v))) + .build()) .option(Option.createBuilder(float.class) .name(Component.translatable("controlify.gui.left_stick_deadzone")) .tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip")) .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .binding(def.leftStickDeadzone, () -> config.leftStickDeadzone, v -> config.leftStickDeadzone = v) - .controller(opt -> new FloatSliderController(opt, 0, 1, 0.02f, v -> Component.literal(String.format("%.0f%%", v*100)))) + .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .build()) .option(Option.createBuilder(float.class) .name(Component.translatable("controlify.gui.right_stick_deadzone")) .tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip")) .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .binding(def.rightStickDeadzone, () -> config.rightStickDeadzone, v -> config.rightStickDeadzone = v) - .controller(opt -> new FloatSliderController(opt, 0, 1, 0.02f, v -> Component.literal(String.format("%.0f%%", v*100)))) + .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .build()) .option(Option.createBuilder(float.class) .name(Component.translatable("controlify.gui.left_trigger_threshold")) diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index b43fe47..0a9a3dc 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -11,6 +11,7 @@ import java.util.Objects; public final class Controller { public static final Map CONTROLLERS = new HashMap<>(); + public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false); private final int id; private final String guid; @@ -21,7 +22,7 @@ public final class Controller { private ControllerState prevState = ControllerState.EMPTY; private final ControllerBindings bindings = new ControllerBindings(this); - private final ControllerConfig config = new ControllerConfig(); + private ControllerConfig config = new ControllerConfig(); public Controller(int id, String guid, String name, boolean gamepad) { this.id = id; @@ -81,6 +82,8 @@ public final class Controller { } public String name() { + if (config().customName != null) + return config().customName; return name; } @@ -92,6 +95,10 @@ public final class Controller { return config; } + public void setConfig(ControllerConfig config) { + this.config = config; + } + @Override public boolean equals(Object obj) { if (obj == this) return true; diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java index 3338258..2101f4e 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java @@ -1,15 +1,13 @@ package dev.isxander.controlify.controller; -import dev.isxander.controlify.config.ControlifyConfig; - public class ControllerConfig { public static final ControllerConfig DEFAULT = new ControllerConfig(); public float horizontalLookSensitivity = 1f; public float verticalLookSensitivity = 0.9f; - public float leftStickDeadzone = 0.2f; - public float rightStickDeadzone = 0.2f; + public float leftStickDeadzone = 0.1f; + public float rightStickDeadzone = 0.1f; // not sure if triggers need deadzones public float leftTriggerDeadzone = 0.0f; @@ -18,21 +16,7 @@ public class ControllerConfig { public float leftTriggerActivationThreshold = 0.5f; public float rightTriggerActivationThreshold = 0.5f; + public int screenRepeatNavigationDelay = 4; + public String customName = null; - - public void notifyChanged() { - ControlifyConfig.save(); - } - - public void overwrite(ControllerConfig from) { - this.horizontalLookSensitivity = from.horizontalLookSensitivity; - this.verticalLookSensitivity = from.verticalLookSensitivity; - this.leftStickDeadzone = from.leftStickDeadzone; - this.rightStickDeadzone = from.rightStickDeadzone; - this.leftTriggerDeadzone = from.leftTriggerDeadzone; - this.rightTriggerDeadzone = from.rightTriggerDeadzone; - this.leftTriggerActivationThreshold = from.leftTriggerActivationThreshold; - this.rightTriggerActivationThreshold = from.rightTriggerActivationThreshold; - this.customName = from.customName; - } } diff --git a/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java index 7ad8402..b541917 100644 --- a/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java +++ b/src/main/java/dev/isxander/controlify/event/ControlifyEvents.java @@ -25,6 +25,12 @@ public class ControlifyEvents { } }); + public static final Event VIRTUAL_MOUSE_TOGGLED = EventFactory.createArrayBacked(VirtualMouseToggled.class, callbacks -> enabled -> { + for (VirtualMouseToggled callback : callbacks) { + callback.onVirtualMouseToggled(enabled); + } + }); + @FunctionalInterface public interface InputModeChanged { void onInputModeChanged(InputMode mode); @@ -39,4 +45,9 @@ public class ControlifyEvents { public interface ControllerBindRegistry { void onRegisterControllerBinds(ControllerBindings bindings, Controller controller); } + + @FunctionalInterface + public interface VirtualMouseToggled { + void onVirtualMouseToggled(boolean enabled); + } } diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSelectionListMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSelectionListMixin.java index 1344c29..190fb8d 100644 --- a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSelectionListMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSelectionListMixin.java @@ -11,6 +11,6 @@ import org.spongepowered.asm.mixin.injection.At; public class AbstractSelectionListMixin { @ModifyExpressionValue(method = "setFocused", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/InputType;isKeyboard()Z")) private boolean shouldEnsureEntryVisible(boolean keyboard) { - return keyboard || Controlify.getInstance().getCurrentInputMode() == InputMode.CONTROLLER; + return keyboard || Controlify.instance().currentInputMode() == InputMode.CONTROLLER; } } diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java index 0145546..0e3575d 100644 --- a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java @@ -2,7 +2,7 @@ package dev.isxander.controlify.mixins.compat.screen.vanilla; import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; -import dev.isxander.controlify.compatibility.screen.component.SliderComponentProcessor; +import dev.isxander.controlify.compatibility.vanilla.SliderComponentProcessor; import net.minecraft.client.gui.components.AbstractSliderButton; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenAccessor.java index 028b979..9982714 100644 --- a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenAccessor.java @@ -14,4 +14,7 @@ public interface ScreenAccessor { @Invoker void invokeChangeFocus(ComponentPath path); + + @Invoker + void invokeClearFocus(); } diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenMixin.java index d2c78a6..55cc6ba 100644 --- a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ScreenMixin.java @@ -12,10 +12,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(Screen.class) public class ScreenMixin implements ScreenProcessorProvider { @Unique - private final ScreenProcessor controlify$processor = new ScreenProcessor((Screen) (Object) this); + private final ScreenProcessor controlify$processor = new ScreenProcessor<>((Screen) (Object) this); @Override - public ScreenProcessor screenProcessor() { + public ScreenProcessor screenProcessor() { return controlify$processor; } diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenAccessor.java new file mode 100644 index 0000000..de15778 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenAccessor.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; +import net.minecraft.client.gui.screens.worldselection.WorldSelectionList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SelectWorldScreen.class) +public interface SelectWorldScreenAccessor { + @Accessor + Button getSelectButton(); + + @Accessor + WorldSelectionList getList(); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenMixin.java new file mode 100644 index 0000000..8c73b17 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/SelectWorldScreenMixin.java @@ -0,0 +1,17 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider; +import dev.isxander.controlify.compatibility.vanilla.SelectWorldScreenProcessor; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(SelectWorldScreen.class) +public class SelectWorldScreenMixin implements ScreenProcessorProvider { + private final SelectWorldScreenProcessor controlify$processor = new SelectWorldScreenProcessor((SelectWorldScreen) (Object) this); + + @Override + public ScreenProcessor screenProcessor() { + return controlify$processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/WorldSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/WorldSelectionListEntryMixin.java new file mode 100644 index 0000000..bbc3db2 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/WorldSelectionListEntryMixin.java @@ -0,0 +1,17 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; +import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; +import dev.isxander.controlify.compatibility.vanilla.WorldListEntryComponentProcessor; +import net.minecraft.client.gui.screens.worldselection.WorldSelectionList; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(WorldSelectionList.WorldListEntry.class) +public class WorldSelectionListEntryMixin implements ComponentProcessorProvider { + private final WorldListEntryComponentProcessor controlify$processor = new WorldListEntryComponentProcessor(); + + @Override + public ComponentProcessor componentProcessor() { + return controlify$processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/core/ClientPacketListenerMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/ClientPacketListenerMixin.java index 493d8fe..3803931 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/ClientPacketListenerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/ClientPacketListenerMixin.java @@ -1,16 +1,10 @@ package dev.isxander.controlify.mixins.core; -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; import dev.isxander.controlify.ingame.ControllerPlayerMovement; import net.minecraft.client.Minecraft; -import net.minecraft.client.Options; import net.minecraft.client.multiplayer.ClientPacketListener; -import net.minecraft.client.player.Input; -import net.minecraft.client.player.KeyboardInput; import net.minecraft.network.protocol.game.ClientboundLoginPacket; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Final; @@ -29,7 +23,7 @@ public class ClientPacketListenerMixin { @Inject(method = "handleLogin", at = @At(value = "FIELD", target = "Lnet/minecraft/client/player/LocalPlayer;input:Lnet/minecraft/client/player/Input;", opcode = Opcodes.ASTORE, shift = At.Shift.AFTER)) private void useControllerInput(ClientboundLoginPacket packet, CallbackInfo ci) { - if (Controlify.getInstance().getCurrentInputMode() == InputMode.CONTROLLER && minecraft.player != null) - minecraft.player.input = new ControllerPlayerMovement(Controlify.getInstance().getCurrentController()); + if (Controlify.instance().currentInputMode() == InputMode.CONTROLLER && minecraft.player != null) + minecraft.player.input = new ControllerPlayerMovement(Controlify.instance().currentController()); } } diff --git a/src/main/java/dev/isxander/controlify/mixins/core/KeyboardHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/KeyboardHandlerMixin.java index 81ab391..6639360 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/KeyboardHandlerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/KeyboardHandlerMixin.java @@ -3,15 +3,22 @@ package dev.isxander.controlify.mixins.core; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.Minecraft; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(KeyboardHandler.class) public class KeyboardHandlerMixin { - @Inject(method = "keyPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setLastInputType(Lnet/minecraft/client/InputType;)V")) - private void onKeyboardInput(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) { - Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + @Shadow @Final private Minecraft minecraft; + + // m_unngxkoe is lambda for GLFW keypress hook - do it outside of the `keyPress` method due to fake inputs + @Inject(method = "m_unngxkoe", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;keyPress(JIIII)V")) + private void onKeyboardInput(long window, int i, int j, int k, int m, CallbackInfo ci) { + if (window == minecraft.getWindow().getWindow()) + Controlify.instance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); } } 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 905976c..38cc7a5 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -11,11 +11,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; public class MinecraftMixin { @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;setup(J)V", shift = At.Shift.AFTER)) private void onInputInitialized(CallbackInfo ci) { - Controlify.getInstance().onInitializeInput(); + Controlify.instance().onInitializeInput(); } @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;turnPlayer()V")) private void doPlayerLook(boolean tick, CallbackInfo ci) { - Controlify.getInstance().getInGameInputHandler().processPlayerLook(); + Controlify.instance().inGameInputHandler().processPlayerLook(); } } diff --git a/src/main/java/dev/isxander/controlify/mixins/core/MouseHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/MouseHandlerMixin.java index a4515f8..2b74afc 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MouseHandlerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MouseHandlerMixin.java @@ -2,26 +2,37 @@ package dev.isxander.controlify.mixins.core; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.InputMode; +import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(MouseHandler.class) public class MouseHandlerMixin { - @Inject(method = "onPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setLastInputType(Lnet/minecraft/client/InputType;)V")) + @Shadow @Final private Minecraft minecraft; + + // m_sljgmtqm is lambda for GLFW mouse click hook - do it outside of the `onPress` method due to fake inputs + @Inject(method = "m_sljgmtqm", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;onPress(JIII)V")) private void onMouseClickInput(long window, int button, int action, int modifiers, CallbackInfo ci) { - Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + if (window == minecraft.getWindow().getWindow()) + Controlify.instance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); } - @Inject(method = "onMove", at = @At("RETURN")) + // m_swhlgdws is lambda for GLFW mouse move hook - do it outside of the `onMove` method due to fake inputs + @Inject(method = "m_swhlgdws", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;onMove(JDD)V")) private void onMouseMoveInput(long window, double x, double y, CallbackInfo ci) { - Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + if (window == minecraft.getWindow().getWindow()) + Controlify.instance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); } - @Inject(method = "onScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getOverlay()Lnet/minecraft/client/gui/screens/Overlay;")) + // m_qoshpwkl is lambda for GLFW mouse scroll hook - do it outside of the `onScroll` method due to fake inputs + @Inject(method = "m_qoshpwkl", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;onScroll(JDD)V")) private void onMouseScrollInput(long window, double scrollDeltaX, double scrollDeltaY, CallbackInfo ci) { - Controlify.getInstance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); + if (window == minecraft.getWindow().getWindow()) + Controlify.instance().setCurrentInputMode(InputMode.KEYBOARD_MOUSE); } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/GameRendererMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/GameRendererMixin.java new file mode 100644 index 0000000..04a0d6d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/GameRendererMixin.java @@ -0,0 +1,18 @@ +package dev.isxander.controlify.mixins.feature.virtualmouse; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.Controlify; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GameRenderer.class) +public class GameRendererMixin { + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;renderWithTooltip(Lcom/mojang/blaze3d/vertex/PoseStack;IIF)V", shift = At.Shift.AFTER)) + private void onPostRenderScreen(float tickDelta, long startTime, boolean tick, CallbackInfo ci, @Local(ordinal = 1) PoseStack poseStack) { + Controlify.instance().virtualMouseHandler().renderVirtualMouse(poseStack); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/KeyboardHandlerAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/KeyboardHandlerAccessor.java new file mode 100644 index 0000000..b032d94 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/KeyboardHandlerAccessor.java @@ -0,0 +1,11 @@ +package dev.isxander.controlify.mixins.feature.virtualmouse; + +import net.minecraft.client.KeyboardHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(KeyboardHandler.class) +public interface KeyboardHandlerAccessor { + @Invoker + void invokeKeyPress(long window, int key, int scancode, int action, int modifiers); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MinecraftMixin.java new file mode 100644 index 0000000..c965f22 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MinecraftMixin.java @@ -0,0 +1,22 @@ +package dev.isxander.controlify.mixins.feature.virtualmouse; + +import dev.isxander.controlify.Controlify; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;updateTitle()V")) + private void onScreenChanged(Screen screen, CallbackInfo ci) { + Controlify.instance().virtualMouseHandler().onScreenChanged(); + } + + @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;turnPlayer()V")) + private void onUpdateMouse(boolean tick, CallbackInfo ci) { + Controlify.instance().virtualMouseHandler().updateMouse(); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerAccessor.java new file mode 100644 index 0000000..f9ab48b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerAccessor.java @@ -0,0 +1,17 @@ +package dev.isxander.controlify.mixins.feature.virtualmouse; + +import net.minecraft.client.MouseHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(MouseHandler.class) +public interface MouseHandlerAccessor { + @Invoker + void invokeOnMove(long window, double x, double y); + + @Invoker + void invokeOnPress(long window, int button, int action, int modifiers); + + @Invoker + void invokeOnScroll(long window, double scrollDeltaX, double scrollDeltaY); +} diff --git a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java new file mode 100644 index 0000000..a2d78e9 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java @@ -0,0 +1,218 @@ +package dev.isxander.controlify.virtualmouse; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.event.ControlifyEvents; +import dev.isxander.controlify.mixins.feature.virtualmouse.KeyboardHandlerAccessor; +import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.lwjgl.glfw.GLFW; + +public class VirtualMouseHandler { + private static final ResourceLocation CURSOR_TEXTURE = new ResourceLocation("controlify", "textures/gui/virtual_mouse.png"); + + private double targetX, targetY; + private double currentX, currentY; + private final Minecraft minecraft; + private boolean virtualMouseEnabled; + + public VirtualMouseHandler() { + this.minecraft = Minecraft.getInstance(); + + ControlifyEvents.INPUT_MODE_CHANGED.register(this::onInputModeChanged); + } + + public void handleControllerInput(Controller controller) { + if (controller.bindings().VMOUSE_TOGGLE.justPressed()) { + toggleVirtualMouse(); + } + + if (!virtualMouseEnabled) { + return; + } + + var leftStickX = controller.state().axes().leftStickX(); + var leftStickY = controller.state().axes().leftStickY(); + + // quadratic function to make small movements smaller + // abs to keep sign + targetX += leftStickX * Mth.abs(leftStickX) * 20f; + targetY += leftStickY * Mth.abs(leftStickY) * 20f; + + targetX = Mth.clamp(targetX, 0, minecraft.getWindow().getWidth()); + targetY = Mth.clamp(targetY, 0, minecraft.getWindow().getHeight()); + + var mouseHandler = (MouseHandlerAccessor) minecraft.mouseHandler; + var keyboardHandler = (KeyboardHandlerAccessor) minecraft.keyboardHandler; + + if (controller.bindings().VMOUSE_LCLICK.justPressed()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_LEFT, GLFW.GLFW_PRESS, 0); + } else if (controller.bindings().VMOUSE_LCLICK.justReleased()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_LEFT, GLFW.GLFW_RELEASE, 0); + } + + if (controller.bindings().VMOUSE_RCLICK.justPressed()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_RIGHT, GLFW.GLFW_PRESS, 0); + } else if (controller.bindings().VMOUSE_RCLICK.justReleased()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_RIGHT, GLFW.GLFW_RELEASE, 0); + } + + if (controller.bindings().VMOUSE_MCLICK.justPressed()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE, GLFW.GLFW_PRESS, 0); + } else if (controller.bindings().VMOUSE_MCLICK.justReleased()) { + mouseHandler.invokeOnPress(minecraft.getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE, GLFW.GLFW_RELEASE, 0); + } + + if (controller.bindings().VMOUSE_ESCAPE.justPressed()) { + keyboardHandler.invokeKeyPress(minecraft.getWindow().getWindow(), GLFW.GLFW_KEY_ESCAPE, 0, GLFW.GLFW_PRESS, 0); + } else if (controller.bindings().VMOUSE_ESCAPE.justReleased()) { + keyboardHandler.invokeKeyPress(minecraft.getWindow().getWindow(), GLFW.GLFW_KEY_ESCAPE, 0, GLFW.GLFW_RELEASE, 0); + } + + // TODO: scrolling with right stick + } + + public void updateMouse() { + if (!virtualMouseEnabled) return; + if (targetX == currentX && targetY == currentY) return; // don't need to needlessly update mouse position + + currentX = Mth.lerp(minecraft.getDeltaFrameTime(), currentX, targetX); + currentY = Mth.lerp(minecraft.getDeltaFrameTime(), currentY, targetY); + + ((MouseHandlerAccessor) minecraft.mouseHandler).invokeOnMove(minecraft.getWindow().getWindow(), currentX, currentY); + } + + public void onScreenChanged() { + if (minecraft.screen != null) { + if (requiresVirtualMouse()) { + enableVirtualMouse(); + } else { + disableVirtualMouse(); + } + if (Controlify.instance().currentInputMode() == InputMode.CONTROLLER) + GLFW.glfwSetInputMode(minecraft.getWindow().getWindow(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_HIDDEN); + } else if (virtualMouseEnabled) { + disableVirtualMouse(); + minecraft.mouseHandler.grabMouse(); + } + } + + public void onInputModeChanged(InputMode mode) { + if (mode == InputMode.CONTROLLER) { + if (requiresVirtualMouse()) { + enableVirtualMouse(); + } + } else if (virtualMouseEnabled) { + disableVirtualMouse(); + } + } + + public void renderVirtualMouse(PoseStack matrices) { + if (!virtualMouseEnabled) return; + + RenderSystem.setShaderTexture(0, CURSOR_TEXTURE); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.enableBlend(); + + var scaledX = currentX * (double)this.minecraft.getWindow().getGuiScaledWidth() / (double)this.minecraft.getWindow().getScreenWidth(); + var scaledY = currentY * (double)this.minecraft.getWindow().getGuiScaledHeight() / (double)this.minecraft.getWindow().getScreenHeight(); + + matrices.pushPose(); + matrices.translate(scaledX, scaledY, 0); + matrices.scale(0.5f, 0.5f, 0.5f); + + GuiComponent.blit(matrices, -16, -16, 0, 0, 32, 32, 32, 32); + + matrices.popPose(); + + RenderSystem.disableBlend(); + } + + public void enableVirtualMouse() { + if (virtualMouseEnabled) return; + + setMousePosition(); + GLFW.glfwSetInputMode(minecraft.getWindow().getWindow(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); + virtualMouseEnabled = true; + + if (minecraft.mouseHandler.xpos() == 0 && minecraft.mouseHandler.ypos() == 0) { + targetX = currentX = minecraft.getWindow().getScreenWidth() / 2f; + targetY = currentY = minecraft.getWindow().getScreenHeight() / 2f; + } else { + targetX = currentX = minecraft.mouseHandler.xpos(); + targetY = currentY = minecraft.mouseHandler.ypos(); + } + + ControlifyEvents.VIRTUAL_MOUSE_TOGGLED.invoker().onVirtualMouseToggled(true); + minecraft.getToasts().addToast(SystemToast.multiline( + minecraft, + SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, + Component.translatable("controlify.toast.vmouse_enabled.title"), + Component.translatable("controlify.toast.vmouse_enabled.description") + )); + } + + public void disableVirtualMouse() { + if (!virtualMouseEnabled) return; + + GLFW.glfwSetInputMode(minecraft.getWindow().getWindow(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + setMousePosition(); + virtualMouseEnabled = false; + targetX = currentX = minecraft.mouseHandler.xpos(); + targetY = currentY = minecraft.mouseHandler.ypos(); + + ControlifyEvents.VIRTUAL_MOUSE_TOGGLED.invoker().onVirtualMouseToggled(false); + minecraft.getToasts().addToast(SystemToast.multiline( + minecraft, + SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, + Component.translatable("controlify.toast.vmouse_disabled.title"), + Component.translatable("controlify.toast.vmouse_disabled.description") + )); + } + + private void setMousePosition() { + GLFW.glfwSetCursorPos( + minecraft.getWindow().getWindow(), + targetX, + targetY + ); + } + + public boolean requiresVirtualMouse() { + return Controlify.instance().currentInputMode() == InputMode.CONTROLLER + && minecraft.screen != null + && (ScreenProcessorProvider.provide(minecraft.screen).forceVirtualMouse() + || Controlify.instance().config().globalSettings().virtualMouseScreens.contains(minecraft.screen.getClass().getName()) + ); + } + + public void toggleVirtualMouse() { + if (minecraft.screen == null) return; + + var screens = Controlify.instance().config().globalSettings().virtualMouseScreens; + var screenName = minecraft.screen.getClass().getName(); + if (screens.contains(screenName)) { + screens.remove(screenName); + disableVirtualMouse(); + Controlify.instance().hideMouse(true); + } else { + screens.add(screenName); + enableVirtualMouse(); + } + + Controlify.instance().config().save(); + } + + public boolean isVirtualMouseEnabled() { + return virtualMouseEnabled; + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 8fcb128..8fbc019 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -1,10 +1,19 @@ { + "controlify.gui.category.global": "Global", + "controlify.gui.current_controller": "Current Controller", + "controlify.gui.current_controller.tooltip": "In Controlify's infancy, only one controller can be used at a time, this selects which one you want to use.", + "controlify.gui.vmouse_screens": "Virtual Mouse Screens", + "controlify.gui.vmouse_screens.tooltip": "A list of Screen class names that require virtual mouse to operate, this is usually due to the screen not being compatible with controller input.", + "controlify.gui.vmouse_screens.placeholder": "Screen class name here...", + "controlify.gui.group.config": "Config", "controlify.gui.group.config.tooltip": "Adjust the controller configuration.", "controlify.gui.horizontal_look_sensitivity": "Horizontal Look Sensitivity", "controlify.gui.horizontal_look_sensitivity.tooltip": "How fast the camera moves horizontally when looking around.", "controlify.gui.vertical_look_sensitivity": "Vertical Look Sensitivity", "controlify.gui.vertical_look_sensitivity.tooltip": "How fast the camera moves vertically when looking around.", + "controlify.gui.screen_repeat_navi_delay": "Screen Repeat Navigation Delay", + "controlify.gui.screen_repeat_navi_delay.tooltip": "How the delay is for navigation to start repeating if you hold the stick.", "controlify.gui.left_stick_deadzone": "Left Stick Deadzone", "controlify.gui.left_stick_deadzone.tooltip": "How far the left joystick needs to be pushed before registering input.", "controlify.gui.right_stick_deadzone": "Right Stick Deadzone", @@ -19,6 +28,17 @@ "controlify.gui.group.controls.tooltip": "Adjust the controller controls.", "controlify.gui.bind_input_awaiting": "Press any button", + "controlify.gui.format.ticks": "%s ticks", + + "controlify.toast.vmouse_enabled.title": "Virtual Mouse Enabled", + "controlify.toast.vmouse_enabled.description": "Controlify virtual mouse is now enabled for this screen.", + "controlify.toast.vmouse_disabled.title": "Virtual Mouse Disabled", + "controlify.toast.vmouse_disabled.description": "Controlify virtual mouse is now disabled for this screen.", + "controlify.toast.controller_connected.title": "Controller Connected", + "controlify.toast.controller_connected.description": "A new controller has just been connected. You can switch to it in Controlify settings.", + "controlify.toast.controller_disconnected.title": "Controller Disconnected", + "controlify.toast.controller_disconnected.description": "'%s' was disconnected.", + "controlify.binding.controlify.jump": "Jump", "controlify.binding.controlify.sneak": "Sneak", "controlify.binding.controlify.attack": "Attack", @@ -32,5 +52,9 @@ "controlify.binding.controlify.open_chat": "Open Chat", "controlify.binding.controlify.gui_press": "GUI Press", "controlify.binding.controlify.gui_back": "GUI Back", - "controlify.binding.controlify.drop": "Drop Item" + "controlify.binding.controlify.drop": "Drop Item", + "controlify.binding.controlify.vmouse_lclick": "Virtual Mouse LClick", + "controlify.binding.controlify.vmouse_rclick": "Virtual Mouse RClick", + "controlify.binding.controlify.vmouse_mclick": "Virtual Mouse MClick", + "controlify.binding.controlify.vmouse_escape": "Virtual Mouse Key Escape" } diff --git a/src/main/resources/assets/controlify/textures/gui/pointer.png b/src/main/resources/assets/controlify/textures/gui/virtual_mouse.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/pointer.png rename to src/main/resources/assets/controlify/textures/gui/virtual_mouse.png diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 97c3c56..76c9005 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -12,10 +12,17 @@ "compat.screen.vanilla.ContainerObjectSelectionListEntryMixin", "compat.screen.vanilla.ScreenAccessor", "compat.screen.vanilla.ScreenMixin", + "compat.screen.vanilla.SelectWorldScreenAccessor", + "compat.screen.vanilla.SelectWorldScreenMixin", + "compat.screen.vanilla.WorldSelectionListEntryMixin", "core.ClientPacketListenerMixin", "core.KeyboardHandlerMixin", "core.MinecraftMixin", "core.MouseHandlerMixin", - "feature.bind.KeyMappingAccessor" + "feature.bind.KeyMappingAccessor", + "feature.virtualmouse.GameRendererMixin", + "feature.virtualmouse.KeyboardHandlerAccessor", + "feature.virtualmouse.MinecraftMixin", + "feature.virtualmouse.MouseHandlerAccessor" ] }