diff --git a/.github/README.md b/.github/README.md index b1353e3..54ed604 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,6 +1,6 @@
-Icon +Icon # Control-ify diff --git a/assets/icon.psd b/assets/icon.psd new file mode 100644 index 0000000..329b795 Binary files /dev/null and b/assets/icon.psd differ diff --git a/build.gradle.kts b/build.gradle.kts index eaf5d85..2582080 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,10 @@ repositories { maven("https://jitpack.io") } +loom { + accessWidenerPath.set(file("src/main/resources/controlify.accesswidener")) +} + val minecraftVersion = libs.versions.minecraft.get() dependencies { @@ -52,10 +56,6 @@ dependencies { include(libs.hid4java) } -machete { - -} - tasks { processResources { val modId: String by project diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0c13b2..f462a88 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ grgit = "5.0.+" minecraft = "23w05a" quilt_mappings = "1" -fabric_loader = "0.14.13" -fabric_api = "0.73.3+1.19.4" +fabric_loader = "0.14.14" +fabric_api = "0.73.4+1.19.4" mixin_extras = "0.2.0-beta.1" yet_another_config_lib = "2.2.0+update.1.19.4-SNAPSHOT" mod_menu = "6.0.0-beta.1" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index b10f756..55ae038 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -36,37 +36,36 @@ public class Controlify { controllerHIDService = new ControllerHIDService(); // find already connected controllers - for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) { + for (int i = 0; i <= GLFW.GLFW_JOYSTICK_LAST; i++) { if (GLFW.glfwJoystickPresent(i)) { int jid = i; - controllerHIDService.awaitNextDevice(device -> { + controllerHIDService.awaitNextController(device -> { setCurrentController(Controller.create(jid, device)); LOGGER.info("Controller found: " + currentController.name()); + config().loadOrCreateControllerData(currentController); }); } } controllerHIDService.start(); - config().load(); // load after initial controller discovery - config().save(); // save new controller configs if they don't exist + config().load(); // listen for new controllers GLFW.glfwSetJoystickCallback((jid, event) -> { if (event == GLFW.GLFW_CONNECTED) { - controllerHIDService.awaitNextDevice(device -> { + controllerHIDService.awaitNextController(device -> { setCurrentController(Controller.create(jid, device)); - LOGGER.info("Controller connected: " + currentController.name() + " (" + device.getPath() + ")"); + LOGGER.info("Controller connected: " + currentController.name()); this.setCurrentInputMode(InputMode.CONTROLLER); - config().load(); // load config again if a configuration already exists for this controller - config().save(); // save config if it doesn't exist + config().loadOrCreateControllerData(currentController); minecraft.getToasts().addToast(SystemToast.multiline( minecraft, SystemToast.SystemToastIds.PERIODIC_NOTIFICATION, Component.translatable("controlify.toast.controller_connected.title"), - Component.translatable("controlify.toast.controller_connected.description") + Component.translatable("controlify.toast.controller_connected.description", currentController.name()) )); }); @@ -98,6 +97,8 @@ public class Controlify { } ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state(); + if (!config().globalSettings().outOfFocusInput && !client.isWindowActive()) + state = ControllerState.EMPTY; if (state.hasAnyInput()) this.setCurrentInputMode(InputMode.CONTROLLER); @@ -108,12 +109,13 @@ public class Controlify { } if (client.screen != null) { - if (!this.virtualMouseHandler().isVirtualMouseEnabled()) - ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); + ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); } else { this.inGameInputHandler().inputTick(); } this.virtualMouseHandler().handleControllerInput(currentController); + + ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(currentController); } public ControlifyConfig config() { diff --git a/src/main/java/dev/isxander/controlify/bindings/Bind.java b/src/main/java/dev/isxander/controlify/bindings/Bind.java index d1b7b53..b8ca800 100644 --- a/src/main/java/dev/isxander/controlify/bindings/Bind.java +++ b/src/main/java/dev/isxander/controlify/bindings/Bind.java @@ -1,21 +1,25 @@ package dev.isxander.controlify.bindings; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.mojang.blaze3d.vertex.PoseStack; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.gui.ButtonRenderer; import net.minecraft.resources.ResourceLocation; import java.util.function.BiFunction; import java.util.function.Function; -public enum Bind { +public enum Bind implements IBind { A_BUTTON(state -> state.buttons().a(), "a_button"), B_BUTTON(state -> state.buttons().b(), "b_button"), X_BUTTON(state -> state.buttons().x(), "x_button"), Y_BUTTON(state -> state.buttons().y(), "y_button"), LEFT_BUMPER(state -> state.buttons().leftBumper(), "left_bumper"), RIGHT_BUMPER(state -> state.buttons().rightBumper(), "right_bumper"), - LEFT_STICK(state -> state.buttons().leftStick(), "left_stick"), - RIGHT_STICK(state -> state.buttons().rightStick(), "right_stick"), + LEFT_STICK_PRESS(state -> state.buttons().leftStick(), "left_stick_press"), + RIGHT_STICK_PRESS(state -> state.buttons().rightStick(), "right_stick_press"), START(state -> state.buttons().start(), "start"), BACK(state -> state.buttons().back(), "back"), GUIDE(state -> state.buttons().guide(), "guide"), // the middle button @@ -23,23 +27,42 @@ public enum Bind { DPAD_DOWN(state -> state.buttons().dpadDown(), "dpad_down"), DPAD_LEFT(state -> state.buttons().dpadLeft(), "dpad_left"), DPAD_RIGHT(state -> state.buttons().dpadRight(), "dpad_right"), - LEFT_TRIGGER((state, controller) -> state.axes().leftTrigger() >= controller.config().leftTriggerActivationThreshold, "left_trigger"), - RIGHT_TRIGGER((state, controller) -> state.axes().rightTrigger() >= controller.config().rightTriggerActivationThreshold, "right_trigger"); + LEFT_TRIGGER((state, controller) -> state.axes().leftTrigger(), "left_trigger"), + RIGHT_TRIGGER((state, controller) -> state.axes().rightTrigger(), "right_trigger"), + LEFT_STICK_FORWARD((state, controller) -> -Math.min(0, state.axes().leftStickY()), "left_stick_up"), + LEFT_STICK_BACKWARD((state, controller) -> Math.max(0, state.axes().leftStickY()), "left_stick_down"), + LEFT_STICK_LEFT((state, controller) -> -Math.min(0, state.axes().leftStickX()), "left_stick_left"), + LEFT_STICK_RIGHT((state, controller) -> Math.max(0, state.axes().leftStickX()), "left_stick_right"), + RIGHT_STICK_FORWARD((state, controller) -> -Math.min(0, state.axes().rightStickY()), "right_stick_up"), + RIGHT_STICK_BACKWARD((state, controller) -> Math.max(0, state.axes().rightStickY()), "right_stick_down"), + RIGHT_STICK_LEFT((state, controller) -> -Math.min(0, state.axes().rightStickX()), "right_stick_left"), + RIGHT_STICK_RIGHT((state, controller) -> Math.max(0, state.axes().rightStickX()), "right_stick_right"); - private final BiFunction state; + private final BiFunction state; private final String identifier; - Bind(BiFunction state, String identifier) { + Bind(BiFunction state, String identifier) { this.state = state; this.identifier = identifier; } Bind(Function state, String identifier) { - this((state1, controller) -> state.apply(state1), identifier); + this((state1, controller) -> state.apply(state1) ? 1f : 0f, identifier); } - public boolean state(ControllerState controllerState, Controller controller) { - return state.apply(controllerState, controller); + @Override + public float state(ControllerState state, Controller controller) { + return this.state.apply(state, controller); + } + + @Override + public void draw(PoseStack matrices, int x, int centerY, Controller controller) { + ButtonRenderer.drawButton(this, controller, matrices, x, centerY); + } + + @Override + public ButtonRenderer.DrawSize drawSize() { + return new ButtonRenderer.DrawSize(22, 22); } public String identifier() { @@ -50,6 +73,11 @@ public enum Bind { return new ResourceLocation("controlify", "textures/gui/buttons/" + controller.config().theme.id() + "/" + identifier + ".png"); } + @Override + public JsonElement toJson() { + return new JsonPrimitive(identifier); + } + public static Bind fromIdentifier(String identifier) { for (Bind bind : values()) { if (bind.identifier.equals(identifier)) return bind; diff --git a/src/main/java/dev/isxander/controlify/bindings/CompoundBind.java b/src/main/java/dev/isxander/controlify/bindings/CompoundBind.java new file mode 100644 index 0000000..3c7e0ba --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/CompoundBind.java @@ -0,0 +1,77 @@ +package dev.isxander.controlify.bindings; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.gui.ButtonRenderer; +import net.minecraft.client.Minecraft; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +public class CompoundBind implements IBind { + private final Set binds; + + CompoundBind(Bind... binds) { + this.binds = new LinkedHashSet<>(Arrays.asList(binds)); + } + + public Set binds() { + return ImmutableSet.copyOf(binds); + } + + @Override + public float state(ControllerState state, Controller controller) { + return held(state, controller) ? 1f : 0f; + } + + @Override + public boolean held(ControllerState state, Controller controller) { + return binds.stream().allMatch(bind -> bind.held(state, controller)); + } + + @Override + public void draw(PoseStack matrices, int x, int centerY, Controller controller) { + var font = Minecraft.getInstance().font; + + var iterator = binds.iterator(); + while (iterator.hasNext()) { + var bind = iterator.next(); + + bind.draw(matrices, x, centerY, controller); + x += bind.drawSize().width(); + + if (iterator.hasNext()) { + font.drawShadow(matrices, "+", x + 1, centerY - font.lineHeight / 2f, 0xFFFFFF); + x += font.width("+") + 2; + } + } + } + + @Override + public ButtonRenderer.DrawSize drawSize() { + return new ButtonRenderer.DrawSize( + binds.stream().map(IBind::drawSize).mapToInt(ButtonRenderer.DrawSize::width).sum() + (binds.size() - 1) * (2 + Minecraft.getInstance().font.width("+")), + binds.stream().map(IBind::drawSize).mapToInt(ButtonRenderer.DrawSize::height).max().orElse(0) + ); + } + + @Override + public JsonElement toJson() { + var list = new JsonArray(); + for (IBind bind : binds) { + list.add(bind.toJson()); + } + return list; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CompoundBind compoundBind && compoundBind.binds.equals(binds) + || obj instanceof Bind bind && Set.of(bind).equals(binds); + } +} diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java index bb4bdd7..9c0d085 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBinding.java @@ -1,52 +1,76 @@ package dev.isxander.controlify.bindings; import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerState; import net.minecraft.client.KeyMapping; +import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + public class ControllerBinding { private final Controller controller; - private Bind bind; - private final Bind defaultBind; + private IBind bind; + private final IBind defaultBind; private final ResourceLocation id; private final Component name, description; private final KeyMapping override; - public ControllerBinding(Controller controller, Bind defaultBind, ResourceLocation id, Component description, KeyMapping override) { + private static final Map> pressedBinds = new HashMap<>(); + + public ControllerBinding(Controller controller, IBind defaultBind, ResourceLocation id, KeyMapping override) { this.controller = controller; this.bind = this.defaultBind = defaultBind; this.id = id; this.name = Component.translatable("controlify.binding." + id.getNamespace() + "." + id.getPath()); - this.description = description; + var descKey = "controlify.binding." + id.getNamespace() + "." + id.getPath() + ".desc"; + this.description = Language.getInstance().has(descKey) ? Component.translatable(descKey) : Component.empty(); this.override = override; } - public ControllerBinding(Controller controller, Bind defaultBind, ResourceLocation id, KeyMapping override) { - this(controller, defaultBind, id, Component.empty(), override); - } - - public boolean held() { + public float state() { return bind.state(controller.state(), controller); } + public boolean held() { + return bind.held(controller.state(), controller); + } + public boolean justPressed() { - return held() && !bind.state(controller.prevState(), controller); + if (hasBindPressed(this)) return false; + + if (held() && !bind.held(controller.prevState(), controller)) { + addPressedBind(this); + return true; + } else { + return false; + } } public boolean justReleased() { - return !held() && bind.state(controller.prevState(), controller); + if (hasBindPressed(this)) return false; + + if (!held() && bind.held(controller.prevState(), controller)) { + addPressedBind(this); + return true; + } else { + return false; + } } - public Bind currentBind() { + public IBind currentBind() { return bind; } - public void setCurrentBind(Bind bind) { + public void setCurrentBind(IBind bind) { this.bind = bind; } - public Bind defaultBind() { + public IBind defaultBind() { return defaultBind; } @@ -65,4 +89,29 @@ public class ControllerBinding { public KeyMapping override() { return override; } + + // FIXME: very hack solution please remove me + + public static void clearPressedBinds(Controller controller) { + if (pressedBinds.containsKey(controller)) { + pressedBinds.get(controller).clear(); + } + } + + private static boolean hasBindPressed(ControllerBinding binding) { + var pressed = pressedBinds.getOrDefault(binding.controller, Set.of()); + return pressed.containsAll(getBinds(binding.bind)); + } + + private static void addPressedBind(ControllerBinding binding) { + pressedBinds.computeIfAbsent(binding.controller, c -> new HashSet<>()).addAll(getBinds(binding.bind)); + } + + private static Set getBinds(IBind bind) { + if (bind instanceof CompoundBind compoundBind) { + return compoundBind.binds(); + } else { + return Set.of((Bind) bind); + } + } } diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index a8b9c95..97771f3 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -14,6 +14,7 @@ import java.util.*; public class ControllerBindings { public final ControllerBinding + WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT, JUMP, SNEAK, ATTACK, USE, SPRINT, @@ -24,18 +25,24 @@ public class ControllerBindings { CHANGE_PERSPECTIVE, OPEN_CHAT, GUI_PRESS, GUI_BACK, - VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_MCLICK, VMOUSE_ESCAPE, VMOUSE_TOGGLE; + GUI_NEXT_TAB, GUI_PREV_TAB, + VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_MCLICK, VMOUSE_ESCAPE, VMOUSE_SHIFT, VMOUSE_TOGGLE, + TOGGLE_DEBUG_MENU; private final Map registry = new LinkedHashMap<>(); public ControllerBindings(Controller controller) { var options = Minecraft.getInstance().options; + register(WALK_FORWARD = new ControllerBinding(controller, Bind.LEFT_STICK_FORWARD, new ResourceLocation("controlify", "walk_forward"), null)); + register(WALK_BACKWARD = new ControllerBinding(controller, Bind.LEFT_STICK_BACKWARD, new ResourceLocation("controlify", "walk_backward"), null)); + register(WALK_LEFT = new ControllerBinding(controller, Bind.LEFT_STICK_LEFT, new ResourceLocation("controlify", "strafe_left"), null)); + register(WALK_RIGHT = new ControllerBinding(controller, Bind.LEFT_STICK_RIGHT, new ResourceLocation("controlify", "strafe_right"), null)); register(JUMP = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "jump"), options.keyJump)); - register(SNEAK = new ControllerBinding(controller, Bind.RIGHT_STICK, new ResourceLocation("controlify", "sneak"), options.keyShift)); + register(SNEAK = new ControllerBinding(controller, Bind.RIGHT_STICK_PRESS, new ResourceLocation("controlify", "sneak"), options.keyShift)); register(ATTACK = new ControllerBinding(controller, Bind.RIGHT_TRIGGER, new ResourceLocation("controlify", "attack"), options.keyAttack)); register(USE = new ControllerBinding(controller, Bind.LEFT_TRIGGER, new ResourceLocation("controlify", "use"), options.keyUse)); - register(SPRINT = new ControllerBinding(controller, Bind.LEFT_STICK, new ResourceLocation("controlify", "sprint"), options.keySprint)); + register(SPRINT = new ControllerBinding(controller, Bind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "sprint"), options.keySprint)); register(DROP = new ControllerBinding(controller, Bind.DPAD_DOWN, new ResourceLocation("controlify", "drop"), options.keyDrop)); register(NEXT_SLOT = new ControllerBinding(controller, Bind.RIGHT_BUMPER, new ResourceLocation("controlify", "next_slot"), null)); register(PREV_SLOT = new ControllerBinding(controller, Bind.LEFT_BUMPER, new ResourceLocation("controlify", "prev_slot"), null)); @@ -45,11 +52,15 @@ 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(GUI_NEXT_TAB = new ControllerBinding(controller, Bind.RIGHT_BUMPER, new ResourceLocation("controlify", "gui_next_tab"), null)); + register(GUI_PREV_TAB = new ControllerBinding(controller, Bind.LEFT_BUMPER, new ResourceLocation("controlify", "gui_prev_tab"), 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_SHIFT = new ControllerBinding(controller, Bind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "vmouse_shift"), null)); register(VMOUSE_TOGGLE = new ControllerBinding(controller, Bind.BACK, new ResourceLocation("controlify", "vmouse_toggle"), null)); + register(TOGGLE_DEBUG_MENU = new ControllerBinding(controller, new CompoundBind(Bind.BACK, Bind.START), new ResourceLocation("controlify", "toggle_debug_menu"), null)); ControlifyEvents.CONTROLLER_BIND_REGISTRY.invoker().onRegisterControllerBinds(this, controller); @@ -73,7 +84,7 @@ public class ControllerBindings { public JsonObject toJson() { JsonObject json = new JsonObject(); for (var binding : registry().values()) { - json.addProperty(binding.id().toString(), binding.currentBind().identifier()); + json.add(binding.id().toString(), binding.currentBind().toJson()); } return json; } @@ -82,13 +93,17 @@ public class ControllerBindings { for (var binding : registry().values()) { var bind = json.get(binding.id().toString()); if (bind == null) continue; - binding.setCurrentBind(Bind.fromIdentifier(bind.getAsString())); + binding.setCurrentBind(IBind.fromJson(bind)); } } private void imitateVanillaClick(Controller controller) { + ControllerBinding.clearPressedBinds(controller); + if (Controlify.instance().currentInputMode() != InputMode.CONTROLLER) return; + if (Minecraft.getInstance().screen != null && !Minecraft.getInstance().screen.passEvents) + return; for (var binding : registry().values()) { KeyMapping vanillaKey = binding.override(); diff --git a/src/main/java/dev/isxander/controlify/bindings/IBind.java b/src/main/java/dev/isxander/controlify/bindings/IBind.java new file mode 100644 index 0000000..8dd3326 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/IBind.java @@ -0,0 +1,38 @@ +package dev.isxander.controlify.bindings; + +import com.google.gson.JsonElement; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.gui.ButtonRenderer; + +import java.util.Collection; + +public interface IBind { + float state(ControllerState state, Controller controller); + default boolean held(ControllerState state, Controller controller) { + return state(state, controller) > controller.config().buttonActivationThreshold; + } + + void draw(PoseStack matrices, int x, int centerY, Controller controller); + ButtonRenderer.DrawSize drawSize(); + + JsonElement toJson(); + + static IBind fromJson(JsonElement json) { + if (json.isJsonArray()) { + return new CompoundBind(json.getAsJsonArray().asList().stream().map(element -> Bind.fromIdentifier(element.getAsString())).toArray(Bind[]::new)); + } else { + return Bind.fromIdentifier(json.getAsString()); + } + } + + static IBind create(Collection binds) { + if (binds.size() == 1) return binds.stream().findAny().orElseThrow(); + return new CompoundBind(binds.toArray(new Bind[0])); + } + static IBind create(Bind... binds) { + if (binds.length == 1) return binds[0]; + return new CompoundBind(binds); + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java index 129e4a9..247594b 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/screen/ScreenProcessor.java @@ -27,8 +27,12 @@ public class ScreenProcessor { } public void onControllerUpdate(Controller controller) { - handleComponentNavigation(controller); - handleButtons(controller); + if (!Controlify.instance().virtualMouseHandler().isVirtualMouseEnabled()) { + handleComponentNavigation(controller); + handleButtons(controller); + } else { + handleVMouseNavigation(controller); + } } public void onInputModeChanged(InputMode mode) { @@ -43,6 +47,9 @@ public class ScreenProcessor { } protected void handleComponentNavigation(Controller controller) { + if (screen.getFocused() == null) + setInitialFocus(); + var focusTree = getFocusTree(); while (!focusTree.isEmpty()) { var focused = focusTree.poll(); @@ -102,6 +109,10 @@ public class ScreenProcessor { screen.onClose(); } + protected void handleVMouseNavigation(Controller controller) { + + } + public void onWidgetRebuild() { setInitialFocus(); } diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/CreativeModeInventoryScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/CreativeModeInventoryScreenProcessor.java new file mode 100644 index 0000000..aff76b4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/CreativeModeInventoryScreenProcessor.java @@ -0,0 +1,31 @@ +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.CreativeModeInventoryScreenAccessor; +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import net.minecraft.world.item.CreativeModeTabs; + +public class CreativeModeInventoryScreenProcessor extends ScreenProcessor { + public CreativeModeInventoryScreenProcessor(CreativeModeInventoryScreen screen) { + super(screen); + } + + @Override + protected void handleVMouseNavigation(Controller controller) { + var accessor = (CreativeModeInventoryScreenAccessor) screen; + + if (controller.bindings().GUI_NEXT_TAB.justPressed()) { + var tabs = CreativeModeTabs.tabs(); + int newIndex = tabs.indexOf(accessor.getSelectedTab()) + 1; + if (newIndex >= tabs.size()) newIndex = 0; + accessor.invokeSelectTab(tabs.get(newIndex)); + } + if (controller.bindings().GUI_PREV_TAB.justPressed()) { + var tabs = CreativeModeTabs.tabs(); + int newIndex = tabs.indexOf(accessor.getSelectedTab()) - 1; + if (newIndex < 0) newIndex = tabs.size() - 1; + accessor.invokeSelectTab(tabs.get(newIndex)); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/JoinMultiplayerScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/JoinMultiplayerScreenProcessor.java new file mode 100644 index 0000000..03f80f1 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/JoinMultiplayerScreenProcessor.java @@ -0,0 +1,25 @@ +package dev.isxander.controlify.compatibility.vanilla; + +import dev.isxander.controlify.compatibility.screen.ScreenProcessor; +import dev.isxander.controlify.controller.Controller; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; + +public class JoinMultiplayerScreenProcessor extends ScreenProcessor { + private final ServerSelectionList list; + + public JoinMultiplayerScreenProcessor(JoinMultiplayerScreen screen, ServerSelectionList list) { + super(screen); + this.list = list; + } + + @Override + protected void handleButtons(Controller controller) { + if (screen.getFocused() instanceof Button && controller.bindings().GUI_BACK.justPressed()) { + screen.setFocused(list); + } + + super.handleButtons(controller); + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/LanguageSelectionListComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/LanguageSelectionListComponentProcessor.java new file mode 100644 index 0000000..896cb46 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/LanguageSelectionListComponentProcessor.java @@ -0,0 +1,35 @@ +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.OptionsSubScreenAccessor; +import net.minecraft.client.Minecraft; + +public class LanguageSelectionListComponentProcessor implements ComponentProcessor { + private final String code; + + public LanguageSelectionListComponentProcessor(String code) { + this.code = code; + } + + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (controller.bindings().GUI_PRESS.justPressed()) { + var minecraft = Minecraft.getInstance(); + var languageManager = minecraft.getLanguageManager(); + if (!code.equals(languageManager.getSelected())) { + languageManager.setSelected(code); + minecraft.options.languageCode = code; + minecraft.reloadResourcePacks(); + minecraft.options.save(); + } + + minecraft.setScreen(((OptionsSubScreenAccessor) screen.screen).getLastScreen()); + + return true; + } + + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/vanilla/ServerSelectionListEntryComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/vanilla/ServerSelectionListEntryComponentProcessor.java new file mode 100644 index 0000000..2c6d956 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/vanilla/ServerSelectionListEntryComponentProcessor.java @@ -0,0 +1,18 @@ +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.JoinMultiplayerScreenAccessor; + +public class ServerSelectionListEntryComponentProcessor implements ComponentProcessor { + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (controller.bindings().GUI_PRESS.justPressed()) { + screen.screen.setFocused(((JoinMultiplayerScreenAccessor) screen.screen).getSelectButton()); + return true; + } + + return true; + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java b/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java new file mode 100644 index 0000000..1df5115 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java @@ -0,0 +1,22 @@ +package dev.isxander.controlify.config; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +public class ClassTypeAdapter implements JsonSerializer>, JsonDeserializer> { + + @Override + public Class deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return Class.forName(json.getAsString()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public JsonElement serialize(Class src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getName()); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index 1d83b45..dedf469 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -17,10 +17,12 @@ public class ControlifyConfig { .serializeNulls() .setPrettyPrinting() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeHierarchyAdapter(Class.class, new ClassTypeAdapter()) .create(); private JsonObject controllerData = new JsonObject(); private GlobalSettings globalSettings = new GlobalSettings(); + private boolean firstLaunch; public void save() { Controlify.LOGGER.info("Saving Controlify config..."); @@ -37,6 +39,7 @@ public class ControlifyConfig { Controlify.LOGGER.info("Loading Controlify config..."); if (!Files.exists(CONFIG_PATH)) { + firstLaunch = true; save(); return; } @@ -55,8 +58,7 @@ public class ControlifyConfig { 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 - newControllerData.add(controller.uid(), generateControllerConfig(controller)); + newControllerData.add(controller.uid().toString(), generateControllerConfig(controller)); } controllerData = newControllerData; @@ -82,15 +84,22 @@ public class ControlifyConfig { JsonObject controllers = object.getAsJsonObject("controllers"); if (controllers != null) { + this.controllerData = controllers; for (var controller : Controller.CONTROLLERS.values()) { - var settings = controllers.getAsJsonObject(controller.uid()); - if (settings != null) { - applyControllerConfig(controller, settings); - } + loadOrCreateControllerData(controller); } } } + public void loadOrCreateControllerData(Controller controller) { + var uid = controller.uid().toString(); + if (controllerData.has(uid)) { + applyControllerConfig(controller, controllerData.getAsJsonObject(uid)); + } else { + save(); + } + } + private void applyControllerConfig(Controller controller, JsonObject object) { controller.setConfig(GSON.fromJson(object.getAsJsonObject("config"), Controller.ControllerConfig.class)); controller.bindings().fromJson(object.getAsJsonObject("bindings")); @@ -99,4 +108,8 @@ public class ControlifyConfig { public GlobalSettings globalSettings() { return globalSettings; } + + public boolean isFirstLaunch() { + return firstLaunch; + } } diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index f40887d..693fbfd 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -1,13 +1,16 @@ package dev.isxander.controlify.config; import com.google.common.collect.Lists; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import java.util.List; public class GlobalSettings { public static final GlobalSettings DEFAULT = new GlobalSettings(); - public List virtualMouseScreens = Lists.newArrayList( - + public List> virtualMouseScreens = Lists.newArrayList( + AbstractContainerScreen.class ); + + public boolean outOfFocusInput = false; } diff --git a/src/main/java/dev/isxander/controlify/config/gui/BindButtonController.java b/src/main/java/dev/isxander/controlify/config/gui/BindButtonController.java index 74066a0..be8e4cd 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/BindButtonController.java +++ b/src/main/java/dev/isxander/controlify/config/gui/BindButtonController.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.config.gui; import com.mojang.blaze3d.vertex.PoseStack; import dev.isxander.controlify.bindings.Bind; +import dev.isxander.controlify.bindings.IBind; import dev.isxander.controlify.compatibility.screen.ScreenProcessor; import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor; import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider; @@ -17,23 +18,27 @@ import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import org.lwjgl.glfw.GLFW; -public class BindButtonController implements Controller { - private final Option option; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +public class BindButtonController implements Controller { + private final Option option; private final dev.isxander.controlify.controller.Controller controller; - public BindButtonController(Option option, dev.isxander.controlify.controller.Controller controller) { + public BindButtonController(Option option, dev.isxander.controlify.controller.Controller controller) { this.option = option; this.controller = controller; } @Override - public Option option() { + public Option option() { return this.option; } @Override public Component formatValue() { - return Component.literal(option().pendingValue().identifier()); + return Component.empty(); } @Override @@ -42,8 +47,9 @@ public class BindButtonController implements Controller { } public static class BindButtonWidget extends ControllerWidget implements ComponentProcessorProvider, ComponentProcessor { - private boolean awaitingControllerInput = false; + private boolean awaitingControllerInput = false, skipFirstTickInput = false; private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC); + private final Set pressedBinds = new LinkedHashSet<>(); public BindButtonWidget(BindButtonController control, YACLScreen screen, Dimension dim) { super(control, screen, dim); @@ -52,9 +58,17 @@ public class BindButtonController implements Controller { @Override protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { if (awaitingControllerInput) { - textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF); + if (pressedBinds.isEmpty()) { + textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF); + } else { + var bind = IBind.create(pressedBinds); + var plusSize = 2 + textRenderer.width("+"); + bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width() - getXPadding() - plusSize, getDimension().centerY(), control.controller); + textRenderer.drawShadow(matrices, "+", getDimension().xLimit() - getXPadding() - plusSize, getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF); + } } else { - ButtonRenderer.drawButton(control.option().pendingValue(), control.controller, matrices, getDimension().xLimit() - ButtonRenderer.BUTTON_SIZE / 2, getDimension().centerY(), ButtonRenderer.BUTTON_SIZE); + var bind = control.option().pendingValue(); + bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width(), getDimension().centerY(), control.controller); } } @@ -84,24 +98,32 @@ public class BindButtonController implements Controller { } @Override - public boolean overrideControllerButtons(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) { - if (!awaitingControllerInput || !isFocused()) return false; - - for (var bind : Bind.values()) { - boolean stateNow = bind.state(controller.state(), controller); - boolean stateBefore = bind.state(controller.prevState(), controller); - if (stateNow && !stateBefore) { - control.option().requestSet(bind); - awaitingControllerInput = false; - return true; - } + public boolean overrideControllerButtons(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) { + if (controller.bindings().GUI_PRESS.justPressed()) { + return awaitingControllerInput = true; } - return false; + if (!awaitingControllerInput) return false; + + if (pressedBinds.stream().anyMatch(bind -> !bind.held(controller.state(), controller))) { + // finished + awaitingControllerInput = false; + control.option().requestSet(IBind.create(pressedBinds)); + pressedBinds.clear(); + } else { + for (var bind : Bind.values()) { + if (bind.held(controller.state(), controller) && !bind.held(controller.prevState(), controller)) { + pressedBinds.add(bind); + } + } + control.controller.consumeButtonState(); + } + + return true; } @Override - public boolean overrideControllerNavigation(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) { + public boolean overrideControllerNavigation(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) { return awaitingControllerInput; } @@ -115,7 +137,7 @@ public class BindButtonController implements Controller { if (awaitingControllerInput) return textRenderer.width(awaitingText); - return ButtonRenderer.BUTTON_SIZE; + return control.option().pendingValue().drawSize().width(); } } } diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index ff04bc7..a7a42db 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -1,28 +1,41 @@ package dev.isxander.controlify.config.gui; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.bindings.Bind; +import dev.isxander.controlify.bindings.IBind; import dev.isxander.controlify.config.GlobalSettings; import dev.isxander.controlify.controller.ControllerTheme; import dev.isxander.controlify.controller.Controller; import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.controllers.ActionController; import dev.isxander.yacl.gui.controllers.TickBoxController; import dev.isxander.yacl.gui.controllers.cycling.CyclingListController; import dev.isxander.yacl.gui.controllers.cycling.EnumController; import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; public class YACLHelper { public static Screen generateConfigScreen(Screen parent) { + if (Controlify.instance().currentController() == null) { + return new AlertScreen( + () -> Minecraft.getInstance().setScreen(parent), + Component.translatable("controlify.gui.error.title").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + Component.translatable("controlify.gui.error.message").withStyle(ChatFormatting.RED) + ); + } + var controlify = Controlify.instance(); var yacl = YetAnotherConfigLib.createBuilder() .title(Component.literal("Controlify")) .save(() -> controlify.config().save()); + var globalSettings = Controlify.instance().config().globalSettings(); var globalCategory = ConfigCategory.createBuilder() .name(Component.translatable("controlify.gui.category.global")) .option(Option.createBuilder(Controller.class) @@ -31,6 +44,17 @@ public class YACLHelper { .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(Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.out_of_focus_input")) + .tooltip(Component.translatable("controlify.gui.out_of_focus_input.tooltip")) + .binding(GlobalSettings.DEFAULT.outOfFocusInput, () -> globalSettings.outOfFocusInput, v -> globalSettings.outOfFocusInput = v) + .controller(TickBoxController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.translatable("controlify.gui.open_issue_tracker")) + .action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues")) + .controller(opt -> new ActionController(opt, Component.translatable("controlify.gui.format.open"))) .build()); yacl.category(globalCategory.build()); @@ -85,15 +109,9 @@ public class YACLHelper { .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")) - .tooltip(Component.translatable("controlify.gui.left_trigger_threshold.tooltip")) - .binding(def.leftTriggerActivationThreshold, () -> config.leftTriggerActivationThreshold, v -> config.leftTriggerActivationThreshold = v) - .controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100)))) - .build()) - .option(Option.createBuilder(float.class) - .name(Component.translatable("controlify.gui.right_trigger_threshold")) - .tooltip(Component.translatable("controlify.gui.right_trigger_threshold.tooltip")) - .binding(def.rightTriggerActivationThreshold, () -> config.rightTriggerActivationThreshold, v -> config.rightTriggerActivationThreshold = v) + .name(Component.translatable("controlify.gui.button_activation_threshold")) + .tooltip(Component.translatable("controlify.gui.button_activation_threshold.tooltip")) + .binding(def.buttonActivationThreshold, () -> config.buttonActivationThreshold, v -> config.buttonActivationThreshold = v) .controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100)))) .build()) .option(Option.createBuilder(ControllerTheme.class) @@ -108,10 +126,12 @@ public class YACLHelper { var controlsGroup = OptionGroup.createBuilder() .name(Component.translatable("controlify.gui.group.controls")); for (var control : controller.bindings().registry().values()) { - controlsGroup.option(Option.createBuilder(Bind.class) + controlsGroup.option(Option.createBuilder(IBind.class) .name(control.name()) .binding(control.defaultBind(), control::currentBind, control::setCurrentBind) .controller(opt -> new BindButtonController(opt, controller)) + .tooltip(control.description()) + .instant(true) .build()); } category.group(controlsGroup.build()); diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index db449af..32dbc95 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -7,19 +7,21 @@ import org.hid4java.HidDevice; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWGamepadState; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.UUID; public final class Controller { public static final Map CONTROLLERS = new HashMap<>(); - public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false, "DUMMY", ControllerType.UNKNOWN); + public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false, UUID.randomUUID(), ControllerType.UNKNOWN); private final int joystickId; private final String guid; private final String name; private final boolean gamepad; - private final String uid; + private final UUID uid; private final ControllerType type; private ControllerState state = ControllerState.EMPTY; @@ -28,7 +30,7 @@ public final class Controller { private final ControllerBindings bindings = new ControllerBindings(this); private ControllerConfig config, defaultConfig; - public Controller(int joystickId, String guid, String name, boolean gamepad, String uid, ControllerType type) { + public Controller(int joystickId, String guid, String name, boolean gamepad, UUID uid, ControllerType type) { this.joystickId = joystickId; this.guid = guid; this.name = name; @@ -62,8 +64,10 @@ public final class Controller { .rightTriggerDeadZone(config().rightTriggerDeadzone); ButtonState buttonState = ButtonState.fromController(this); state = new ControllerState(axesState, buttonState); + } - ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(this); + public void consumeButtonState() { + this.state = new ControllerState(state().axes(), ButtonState.EMPTY); } public ControllerBindings bindings() { @@ -89,7 +93,7 @@ public final class Controller { return guid; } - public String uid() { + public UUID uid() { return uid; } @@ -141,7 +145,7 @@ public final class Controller { String guid = GLFW.glfwGetJoystickGUID(id); boolean gamepad = GLFW.glfwJoystickIsGamepad(id); String fallbackName = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id); - String uid = device.getPath(); + UUID uid = UUID.nameUUIDFromBytes(device.getPath().getBytes(StandardCharsets.UTF_8)); ControllerType type = ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId())); String name = type != ControllerType.UNKNOWN || fallbackName == null ? type.friendlyName() : fallbackName; int tries = 1; @@ -166,8 +170,7 @@ public final class Controller { public float leftTriggerDeadzone = 0.0f; public float rightTriggerDeadzone = 0.0f; - public float leftTriggerActivationThreshold = 0.5f; - public float rightTriggerActivationThreshold = 0.5f; + public float buttonActivationThreshold = 0.5f; public int screenRepeatNavigationDelay = 4; diff --git a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java index 64f5b75..c5844bb 100644 --- a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java +++ b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.controller.hid; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.ControllerType; import org.hid4java.*; import org.hid4java.event.HidServicesEvent; @@ -35,7 +36,7 @@ public class ControllerHIDService implements HidServicesListener { services.start(); } - public void awaitNextDevice(Consumer consumer) { + public void awaitNextController(Consumer consumer) { deviceQueue.add(consumer); } @@ -46,6 +47,8 @@ public class ControllerHIDService implements HidServicesListener { if (isController(device)) { if (deviceQueue.peek() != null) { deviceQueue.poll().accept(event.getHidDevice()); + } else { + Controlify.LOGGER.error("Unhandled controller: " + ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId())).friendlyName()); } } } diff --git a/src/main/java/dev/isxander/controlify/gui/ButtonRenderer.java b/src/main/java/dev/isxander/controlify/gui/ButtonRenderer.java index 4f27501..c4e6f5c 100644 --- a/src/main/java/dev/isxander/controlify/gui/ButtonRenderer.java +++ b/src/main/java/dev/isxander/controlify/gui/ButtonRenderer.java @@ -9,10 +9,12 @@ import net.minecraft.client.gui.GuiComponent; public class ButtonRenderer { public static final int BUTTON_SIZE = 22; - public static void drawButton(Bind button, Controller controller, PoseStack poseStack, int x, int y, int size) { + public static void drawButton(Bind button, Controller controller, PoseStack poseStack, int x, int centerY) { RenderSystem.setShaderTexture(0, button.textureLocation(controller)); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - GuiComponent.blit(poseStack, x - size / 2, y - size / 2, 0, 0, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE); + GuiComponent.blit(poseStack, x, centerY - BUTTON_SIZE / 2, 0, 0, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE); } + + public record DrawSize(int width, int height) { } } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/BetaNoticeScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/BetaNoticeScreen.java new file mode 100644 index 0000000..2783783 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/BetaNoticeScreen.java @@ -0,0 +1,63 @@ +package dev.isxander.controlify.gui.screen; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.gui.components.AccessibilityOnboardingTextWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.MultiLineTextWidget; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.GridLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; + +public class BetaNoticeScreen extends Screen { + private MultiLineTextWidget textWidget; + + public BetaNoticeScreen() { + super(Component.translatable("controlify.beta.title")); + } + + @Override + protected void init() { + textWidget = new AccessibilityOnboardingTextWidget( + font, + Component.translatable("controlify.beta.message", + Component.translatable("controlify.beta.message.link") + .withStyle(ChatFormatting.AQUA) + ), + this.width - 10 + ); + textWidget.setX(this.width / 2 - textWidget.getWidth() / 2); + textWidget.setY(30); + addRenderableWidget(textWidget); + + addRenderableWidget( + Button.builder( + Component.translatable("controlify.beta.button"), + btn -> Util.getPlatform().openUri("https://github.com/isXander/controlify/issues") + ) + .pos(this.width / 2 - 75, this.height - 8 - 20 - 20 - 4) + .width(150) + .build() + ); + addRenderableWidget( + Button.builder( + CommonComponents.GUI_CONTINUE, + btn -> minecraft.setScreen(new TitleScreen()) + ) + .pos(this.width / 2 - 75, this.height - 8 - 20) + .width(150) + .build() + ); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + renderBackground(matrices); + super.render(matrices, mouseX, mouseY, delta); + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java index b4ddfc0..1d87458 100644 --- a/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java +++ b/src/main/java/dev/isxander/controlify/ingame/ControllerPlayerMovement.java @@ -25,25 +25,23 @@ public class ControllerPlayerMovement extends Input { return; } - var axes = controller.state().axes(); + var bindings = controller.bindings(); - this.up = axes.leftStickY() < 0; - this.down = axes.leftStickY() > 0; - this.left = axes.leftStickX() < 0; - this.right = axes.leftStickX() > 0; - this.leftImpulse = -axes.leftStickX(); - this.forwardImpulse = -axes.leftStickY(); + this.forwardImpulse = bindings.WALK_FORWARD.state() - bindings.WALK_BACKWARD.state(); + this.leftImpulse = bindings.WALK_LEFT.state() - bindings.WALK_RIGHT.state(); + + // .1 to prevent using boat turning absolute hell with left/right left/right + this.up = bindings.WALK_FORWARD.state() > 0.1; + this.down = bindings.WALK_BACKWARD.state() > 0.1; + this.left = bindings.WALK_LEFT.state() > 0.1; + this.right = bindings.WALK_RIGHT.state() > 0.1; if (slowDown) { this.leftImpulse *= f; this.forwardImpulse *= f; } - var bindings = controller.bindings(); - this.jumping = bindings.JUMP.held(); - if (bindings.SNEAK.justPressed()) { - this.shiftKeyDown = !this.shiftKeyDown; - } + this.shiftKeyDown = bindings.SNEAK.held(); } } diff --git a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java index ce81f6b..6e8a677 100644 --- a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java +++ b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java @@ -34,17 +34,20 @@ public class InGameInputHandler { } public void inputTick() { - var axes = controller.state().axes(); - if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive()) { - accumulatedDX += axes.rightStickX(); - accumulatedDY += axes.rightStickY(); - } + handlePlayerLookInput(); + handleKeybinds(); + } - processPlayerLook(); + protected void handleKeybinds() { + if (Minecraft.getInstance().screen != null && !Minecraft.getInstance().screen.passEvents) + return; if (controller.bindings().PAUSE.justPressed()) { minecraft.pauseGame(false); } + if (controller.bindings().TOGGLE_DEBUG_MENU.justPressed()) { + minecraft.options.renderDebug = !minecraft.options.renderDebug; + } if (minecraft.player != null) { if (controller.bindings().NEXT_SLOT.justPressed()) { minecraft.player.getInventory().swapPaint(-1); @@ -55,6 +58,16 @@ public class InGameInputHandler { } } + protected void handlePlayerLookInput() { + var axes = controller.state().axes(); + if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive()) { + accumulatedDX += axes.rightStickX(); + accumulatedDY += axes.rightStickY(); + } + + processPlayerLook(); + } + public void processPlayerLook() { var time = Blaze3D.getTime(); var delta = time - deltaTime; diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java index 0e3575d..4ff1127 100644 --- a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/AbstractSliderButtonMixin.java @@ -1,12 +1,17 @@ package dev.isxander.controlify.mixins.compat.screen.vanilla; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +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.vanilla.SliderComponentProcessor; +import net.minecraft.client.InputType; import net.minecraft.client.gui.components.AbstractSliderButton; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; /** * Mixin to insert a custom {@link ComponentProcessor} into slider to support left/right movement without navigating to next component. @@ -22,6 +27,13 @@ public class AbstractSliderButtonMixin implements ComponentProcessorProvider { val -> this.canChangeValue = val ); + @ModifyExpressionValue(method = "setFocused", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getLastInputType()Lnet/minecraft/client/InputType;")) + private InputType shouldChangeValue(InputType type) { + if (Controlify.instance().currentInputMode() == InputMode.CONTROLLER) + return InputType.NONE; // none doesn't pass condition + return type; + } + @Override public ComponentProcessor componentProcessor() { return controlify$processor; diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenAccessor.java new file mode 100644 index 0000000..3fe873e --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenAccessor.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import net.minecraft.world.item.CreativeModeTab; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(CreativeModeInventoryScreen.class) +public interface CreativeModeInventoryScreenAccessor { + @Accessor + CreativeModeTab getSelectedTab(); + + @Invoker + void invokeSelectTab(CreativeModeTab tab); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenMixin.java new file mode 100644 index 0000000..d795b7b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/CreativeModeInventoryScreenMixin.java @@ -0,0 +1,19 @@ +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.CreativeModeInventoryScreenProcessor; +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(CreativeModeInventoryScreen.class) +public class CreativeModeInventoryScreenMixin implements ScreenProcessorProvider { + @Unique private final CreativeModeInventoryScreenProcessor controlify$screenProcessor + = new CreativeModeInventoryScreenProcessor((CreativeModeInventoryScreen) (Object) this); + + @Override + public ScreenProcessor screenProcessor() { + return controlify$screenProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenAccessor.java new file mode 100644 index 0000000..9b953a4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenAccessor.java @@ -0,0 +1,12 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(JoinMultiplayerScreen.class) +public interface JoinMultiplayerScreenAccessor { + @Accessor + Button getSelectButton(); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenMixin.java new file mode 100644 index 0000000..910b62f --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/JoinMultiplayerScreenMixin.java @@ -0,0 +1,23 @@ +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.JoinMultiplayerScreenProcessor; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(JoinMultiplayerScreen.class) +public class JoinMultiplayerScreenMixin implements ScreenProcessorProvider { + @Shadow protected ServerSelectionList serverSelectionList; + + @Unique private final JoinMultiplayerScreenProcessor controlify$processor + = new JoinMultiplayerScreenProcessor((JoinMultiplayerScreen) (Object) this, serverSelectionList); + + @Override + public ScreenProcessor screenProcessor() { + return controlify$processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/LanguageSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/LanguageSelectionListEntryMixin.java new file mode 100644 index 0000000..6c184ad --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/LanguageSelectionListEntryMixin.java @@ -0,0 +1,26 @@ +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.LanguageSelectionListComponentProcessor; +import net.minecraft.client.gui.screens.LanguageSelectScreen; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(LanguageSelectScreen.LanguageSelectionList.Entry.class) +public class LanguageSelectionListEntryMixin implements ComponentProcessorProvider { + @Shadow @Final String code; + + @Unique private LanguageSelectionListComponentProcessor controlify$componentProcessor = null; + + @Override + public ComponentProcessor componentProcessor() { + // lazily create the component processor so `code` is defined + if (controlify$componentProcessor == null) + controlify$componentProcessor = new LanguageSelectionListComponentProcessor(code); + + return controlify$componentProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/OptionsSubScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/OptionsSubScreenAccessor.java new file mode 100644 index 0000000..61437fb --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/OptionsSubScreenAccessor.java @@ -0,0 +1,12 @@ +package dev.isxander.controlify.mixins.compat.screen.vanilla; + +import net.minecraft.client.gui.screens.OptionsSubScreen; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(OptionsSubScreen.class) +public interface OptionsSubScreenAccessor { + @Accessor + Screen getLastScreen(); +} diff --git a/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ServerSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ServerSelectionListEntryMixin.java new file mode 100644 index 0000000..8ab2ca3 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/compat/screen/vanilla/ServerSelectionListEntryMixin.java @@ -0,0 +1,21 @@ +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.ServerSelectionListEntryComponentProcessor; +import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerSelectionList.Entry.class) +public class ServerSelectionListEntryMixin implements ComponentProcessorProvider { + @Unique private final ServerSelectionListEntryComponentProcessor controlify$componentProcessor + = new ServerSelectionListEntryComponentProcessor(); + + @Override + public ComponentProcessor componentProcessor() { + return ((ServerSelectionList.Entry) (Object) this) instanceof ServerSelectionList.LANHeader + ? ComponentProcessor.EMPTY + : controlify$componentProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java index 38cc7a5..990d7d5 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -1,14 +1,21 @@ package dev.isxander.controlify.mixins.core; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.gui.screen.BetaNoticeScreen; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.main.GameConfig; +import org.jetbrains.annotations.Nullable; 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(Minecraft.class) -public class MinecraftMixin { +public abstract class MinecraftMixin { + @Shadow public abstract void setScreen(@Nullable Screen screen); + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;setup(J)V", shift = At.Shift.AFTER)) private void onInputInitialized(CallbackInfo ci) { Controlify.instance().onInitializeInput(); @@ -18,4 +25,10 @@ public class MinecraftMixin { private void doPlayerLook(boolean tick, CallbackInfo ci) { Controlify.instance().inGameInputHandler().processPlayerLook(); } + + @Inject(method = "", at = @At("TAIL")) + private void showBetaScreen(GameConfig args, CallbackInfo ci) { + if (Controlify.instance().config().isFirstLaunch()) + setScreen(new BetaNoticeScreen()); + } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/InputConstantsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/InputConstantsMixin.java new file mode 100644 index 0000000..ac01da4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/InputConstantsMixin.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.mixins.feature.virtualmouse; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.controlify.Controlify; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(InputConstants.class) +public class InputConstantsMixin { + // must modify isKeyDown here because Screen.hasShiftDown has some instances that ask for this directly. + @ModifyReturnValue(method = "isKeyDown", at = @At("RETURN")) + private static boolean modifyIsKeyDown(boolean keyDown, long window, int key) { + if (key == GLFW.GLFW_KEY_LEFT_SHIFT) { + return keyDown || Controlify.instance().currentController().bindings().VMOUSE_SHIFT.held(); + } + + return keyDown; + } +} diff --git a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java index 748681e..3203edd 100644 --- a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java +++ b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java @@ -180,20 +180,21 @@ public class VirtualMouseHandler { } 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()) - ); + var isController = Controlify.instance().currentInputMode() == InputMode.CONTROLLER; + var hasScreen = minecraft.screen != null; + var forceVirtualMouse = hasScreen && ScreenProcessorProvider.provide(minecraft.screen).forceVirtualMouse(); + var screenIsVMouseScreen = hasScreen && Controlify.instance().config().globalSettings().virtualMouseScreens.stream().anyMatch(s -> s.isAssignableFrom(minecraft.screen.getClass())); + + return isController && hasScreen && (forceVirtualMouse || screenIsVMouseScreen); } 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); + var screenClass = minecraft.screen.getClass(); + if (screens.contains(screenClass)) { + screens.remove(screenClass); disableVirtualMouse(); Controlify.instance().hideMouse(true); @@ -204,7 +205,7 @@ public class VirtualMouseHandler { Component.translatable("controlify.toast.vmouse_disabled.description") )); } else { - screens.add(screenName); + screens.add(screenClass); enableVirtualMouse(); minecraft.getToasts().addToast(SystemToast.multiline( diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 412d119..d00c16e 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -2,9 +2,9 @@ "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.out_of_focus_input": "Out of Focus Input", + "controlify.gui.out_of_focus_input.tooltip": "If enabled, Controlify will still receive input even if the game window is not focused.", + "controlify.gui.open_issue_tracker": "Open Issue Tracker", "controlify.gui.group.config": "Config", "controlify.gui.group.config.tooltip": "Adjust the controller configuration.", @@ -21,10 +21,8 @@ "controlify.gui.right_stick_deadzone": "Right Stick Deadzone", "controlify.gui.right_stick_deadzone.tooltip": "How far the right joystick needs to be pushed before registering input.", "controlify.gui.stickdrift_warning": "Warning: Setting this too low will cause stickdrift! This is where the internals of your controller become mis-calibrated and register small amounts of input when there shouldn't be.", - "controlify.gui.left_trigger_threshold": "Left Trigger Threshold", - "controlify.gui.left_trigger_threshold.tooltip": "How far the left trigger needs to be pushed before registering as pressed.", - "controlify.gui.right_trigger_threshold": "Right Trigger Threshold", - "controlify.gui.right_trigger_threshold.tooltip": "How far the right trigger needs to be pushed before registering as pressed.", + "controlify.gui.button_activation_threshold": "Button Activation Threshold", + "controlify.gui.button_activation_threshold.tooltip": "How far a button needs to be pushed before registering as pressed.", "controlify.gui.controller_theme": "Controller Theme", "controlify.gui.controller_theme.tooltip": "The theme to use for rendering controller buttons.", @@ -33,6 +31,10 @@ "controlify.gui.bind_input_awaiting": "Press any button", "controlify.gui.format.ticks": "%s ticks", + "controlify.gui.format.open": "OPEN URL", + + "controlify.gui.error.title": "Could not open Controlify settings", + "controlify.gui.error.message": "You cannot change Controlify setttings when you have no controllers connected. Please connect a controller first.", "controlify.gui.button": "Controller Settings...", @@ -48,6 +50,10 @@ "controlify.controller_theme.xbox_one": "Xbox", "controlify.controller_theme.dualshock4": "PS4", + "controlify.binding.controlify.walk_forward": "Walk Forward", + "controlify.binding.controlify.walk_backward": "Walk Backward", + "controlify.binding.controlify.strafe_left": "Strafe Left", + "controlify.binding.controlify.strafe_right": "Strafe Right", "controlify.binding.controlify.jump": "Jump", "controlify.binding.controlify.sneak": "Sneak", "controlify.binding.controlify.attack": "Attack", @@ -61,10 +67,19 @@ "controlify.binding.controlify.open_chat": "Open Chat", "controlify.binding.controlify.gui_press": "GUI Press", "controlify.binding.controlify.gui_back": "GUI Back", + "controlify.binding.controlify.gui_next_tab": "GUI Next Tab", + "controlify.binding.controlify.gui_prev_tab": "GUI Previous Tab", "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", - "controlify.binding.controlify.vmouse_toggle": "Toggle Virtual Mouse" + "controlify.binding.controlify.vmouse_shift": "Virtual Mouse Key Shift", + "controlify.binding.controlify.vmouse_toggle": "Toggle Virtual Mouse", + "controlify.binding.controlify.toggle_debug_menu": "Toggle F3 Menu", + + "controlify.beta.title": "Controlify Beta Notice", + "controlify.beta.message": "You are currently using Controlify Beta.\n\nThis mod is a work in progress and will contain many bugs. Please, if you spot a bug in this mod or have a suggestion to make it even better, please create an issue on the %s!\n\nYou can always find the link to the issue tracker in Controlify's settings menu.", + "controlify.beta.message.link": "issue tracker", + "controlify.beta.button": "Open Issue Tracker..." } diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_down.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_down.png new file mode 100644 index 0000000..e300c05 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_down.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_left.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_left.png new file mode 100644 index 0000000..da7516f Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_left.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_press.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick.png rename to src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_press.png diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_right.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_right.png new file mode 100644 index 0000000..0cfabff Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_right.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_up.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_up.png new file mode 100644 index 0000000..d7067a3 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/left_stick_up.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_down.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_down.png new file mode 100644 index 0000000..43010e0 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_down.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_left.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_left.png new file mode 100644 index 0000000..3be5fed Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_left.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_press.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick.png rename to src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_press.png diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_right.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_right.png new file mode 100644 index 0000000..d22c0a6 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_right.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_up.png b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_up.png new file mode 100644 index 0000000..bc54864 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/dualshock4/right_stick_up.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_down.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_down.png new file mode 100644 index 0000000..feaf94e Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_down.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_left.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_left.png new file mode 100644 index 0000000..a80d177 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_left.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_press.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick.png rename to src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_press.png diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_right.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_right.png new file mode 100644 index 0000000..5b35903 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_right.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_up.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_up.png new file mode 100644 index 0000000..15329f4 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/left_stick_up.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_down.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_down.png new file mode 100644 index 0000000..cd52288 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_down.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_left.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_left.png new file mode 100644 index 0000000..dcda058 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_left.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_press.png similarity index 100% rename from src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick.png rename to src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_press.png diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_right.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_right.png new file mode 100644 index 0000000..452c230 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_right.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_up.png b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_up.png new file mode 100644 index 0000000..7aa84d7 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/buttons/xbox/right_stick_up.png differ diff --git a/src/main/resources/controlify.accessWidener b/src/main/resources/controlify.accessWidener new file mode 100644 index 0000000..d60b1d8 --- /dev/null +++ b/src/main/resources/controlify.accessWidener @@ -0,0 +1,3 @@ +accessWidener v2 named + +accessible class net/minecraft/client/gui/screens/LanguageSelectScreen$LanguageSelectionList diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index fc83df1..0a7b619 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -10,10 +10,17 @@ "compat.screen.vanilla.AbstractSelectionListMixin", "compat.screen.vanilla.AbstractSliderButtonMixin", "compat.screen.vanilla.ContainerObjectSelectionListEntryMixin", + "compat.screen.vanilla.CreativeModeInventoryScreenAccessor", + "compat.screen.vanilla.CreativeModeInventoryScreenMixin", + "compat.screen.vanilla.JoinMultiplayerScreenAccessor", + "compat.screen.vanilla.JoinMultiplayerScreenMixin", + "compat.screen.vanilla.LanguageSelectionListEntryMixin", + "compat.screen.vanilla.OptionsSubScreenAccessor", "compat.screen.vanilla.ScreenAccessor", "compat.screen.vanilla.ScreenMixin", "compat.screen.vanilla.SelectWorldScreenAccessor", "compat.screen.vanilla.SelectWorldScreenMixin", + "compat.screen.vanilla.ServerSelectionListEntryMixin", "compat.screen.vanilla.WorldSelectionListEntryMixin", "core.ClientPacketListenerMixin", "core.KeyboardHandlerMixin", @@ -22,6 +29,7 @@ "feature.bind.KeyMappingAccessor", "feature.settingsbutton.ControlsScreenMixin", "feature.virtualmouse.GameRendererMixin", + "feature.virtualmouse.InputConstantsMixin", "feature.virtualmouse.KeyboardHandlerAccessor", "feature.virtualmouse.MinecraftMixin", "feature.virtualmouse.MouseHandlerAccessor", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index ed0c907..0e61efa 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "mixins": [ "controlify.mixins.json" ], + "accessWidener": "controlify.accesswidener", "depends": { "fabricloader": ">=0.14.0", "minecraft": "~1.19.4-", diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png new file mode 100644 index 0000000..da98edc Binary files /dev/null and b/src/main/resources/icon.png differ