1
0
forked from Clones/Controlify

virtual mouse + singleplayer screen compat + 22w05a

This commit is contained in:
isXander
2023-02-02 21:36:44 +00:00
parent 57fcd0c0dc
commit 09628defc4
34 changed files with 697 additions and 111 deletions

View File

@ -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"

View File

@ -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) {
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;
}

View File

@ -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<ResourceLocation, ControllerBinding> 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()) {

View File

@ -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<T extends Screen> {
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<GuiEventListener> getFocusTree() {
if (screen.getFocused() == null) return new ArrayDeque<>();
@ -110,6 +135,8 @@ public class ScreenProcessor {
tree.add(focused);
while (focused instanceof CustomFocus customFocus) {
focused = customFocus.getCustomFocus();
if (focused != null)
tree.addFirst(focused);
}

View File

@ -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();
}
}

View File

@ -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) {
}
}

View File

@ -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<SelectWorldScreen> {
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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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) {
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 = object.getAsJsonObject(controller.guid());
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;
}
}

View File

@ -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<String> virtualMouseScreens = Lists.newArrayList(
);
}

View File

@ -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"))

View File

@ -11,6 +11,7 @@ import java.util.Objects;
public final class Controller {
public static final Map<Integer, Controller> 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;

View File

@ -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;
}
}

View File

@ -25,6 +25,12 @@ public class ControlifyEvents {
}
});
public static final Event<VirtualMouseToggled> 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);
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -14,4 +14,7 @@ public interface ScreenAccessor {
@Invoker
void invokeChangeFocus(ComponentPath path);
@Invoker
void invokeClearFocus();
}

View File

@ -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<Screen> controlify$processor = new ScreenProcessor<>((Screen) (Object) this);
@Override
public ScreenProcessor screenProcessor() {
public ScreenProcessor<Screen> screenProcessor() {
return controlify$processor;
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -11,11 +11,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class MinecraftMixin {
@Inject(method = "<init>", 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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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"
}

View File

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 861 B

View File

@ -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"
]
}