forked from Clones/Controlify
virtual mouse + singleplayer screen compat + 22w05a
This commit is contained in:
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
||||
);
|
||||
}
|
@ -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"))
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -14,4 +14,7 @@ public interface ScreenAccessor {
|
||||
|
||||
@Invoker
|
||||
void invokeChangeFocus(ComponentPath path);
|
||||
|
||||
@Invoker
|
||||
void invokeClearFocus();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 861 B After Width: | Height: | Size: 861 B |
@ -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"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user