From 26d797df42a0033f52fb74623569640974fab23a Mon Sep 17 00:00:00 2001 From: TotallyDem Date: Thu, 2 Jul 2026 23:55:05 -0700 Subject: [PATCH 1/3] Adding multiple input support It's currently a little rough around the edges. Notably, the icons for + between inputs in the UI are missing. --- .../controlify/api/bind/InputBinding.java | 8 ++++ .../controlify/bindings/InputBindingImpl.java | 6 +++ .../bindings/input/CompoundInput.java | 36 +++++++++++++++++ .../controlify/bindings/input/InputType.java | 3 +- .../controller/input/InputComponent.java | 18 +++++++++ .../gui/controllers/BindController.java | 24 +++++++----- .../gui/screen/BindConsumerScreen.java | 39 +++++++++++++++---- 7 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/bindings/input/CompoundInput.java diff --git a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java index b0b75ea51..e951b6b58 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java +++ b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java @@ -2,6 +2,7 @@ import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.StateAccess; +import dev.isxander.controlify.bindings.input.CompoundInput; import dev.isxander.controlify.bindings.input.EmptyInput; import dev.isxander.controlify.bindings.input.Input; import dev.isxander.controlify.bindings.output.AnalogueOutput; @@ -100,6 +101,13 @@ default Component inputIcon() { */ void fakePress(); + /** + * Prevents this binding from being used. Used every state update where the bind is meant to be suppressed. + * This is used to prevent individual bindings from being activated when a {@link CompoundInput} is being used. + * Does not prevent {@link #fakePress()} from being used. + */ + void limitActivity(); + /** * Set the input that this binding is bound to. * diff --git a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java index 81ac75d94..3038bbfea 100644 --- a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java @@ -178,6 +178,12 @@ public void fakePress() { fakePressState = 0; } + @Override + public void limitActivity() { + if (fakePressState == -1) + fakePressState = 3; + } + @Override public void setBoundInput(Input input) { if (inputComponent != null) { diff --git a/src/main/java/dev/isxander/controlify/bindings/input/CompoundInput.java b/src/main/java/dev/isxander/controlify/bindings/input/CompoundInput.java new file mode 100644 index 000000000..3f7c5df0c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/input/CompoundInput.java @@ -0,0 +1,36 @@ +package dev.isxander.controlify.bindings.input; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.controller.input.ControllerStateView; +import net.minecraft.resources.Identifier; + +import java.util.List; + +public record CompoundInput(List inputs) implements Input { + public static final String INPUT_ID = "compound"; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + Codec.lazyInitialized(()-> Input.CODEC).listOf().fieldOf(INPUT_ID).forGetter(CompoundInput::inputs) + ).apply(instance, CompoundInput::new)); + + + @Override + public float state(ControllerStateView state) { + for (Input input : inputs) { + if (input.state(state) == 0) return 0; + } + return 1; + } + + @Override + public List getRelevantInputs() { + return inputs.stream().flatMap(input -> input.getRelevantInputs().stream()).toList(); + } + + @Override + public InputType type() { + return InputType.COMPOUND; + } +} diff --git a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java index e9367f965..ec959e6bc 100644 --- a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java +++ b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java @@ -19,10 +19,11 @@ public record InputType(String id, MapCodec codec) implement public static final InputType BUTTON = new InputType<>(ButtonInput.INPUT_ID, ButtonInput.CODEC); public static final InputType AXIS = new InputType<>(AxisInput.INPUT_ID, AxisInput.CODEC); public static final InputType HAT = new InputType<>(HatInput.INPUT_ID, HatInput.CODEC); + public static final InputType COMPOUND = new InputType(CompoundInput.INPUT_ID, CompoundInput.CODEC); public static final InputType EMPTY = new InputType<>(EmptyInput.INPUT_ID, EmptyInput.CODEC); public static final InputType[] TYPES = { - InputType.BUTTON, InputType.AXIS, InputType.HAT, InputType.EMPTY + InputType.BUTTON, InputType.AXIS, InputType.HAT, InputType.COMPOUND, InputType.EMPTY }; public static MapCodec createCodec( diff --git a/src/main/java/dev/isxander/controlify/controller/input/InputComponent.java b/src/main/java/dev/isxander/controlify/controller/input/InputComponent.java index f5d01496d..0f9e8595b 100644 --- a/src/main/java/dev/isxander/controlify/controller/input/InputComponent.java +++ b/src/main/java/dev/isxander/controlify/controller/input/InputComponent.java @@ -3,6 +3,7 @@ import dev.isxander.controlify.Controlify; import dev.isxander.controlify.bindings.ControlifyBindApiImpl; import dev.isxander.controlify.api.bind.InputBinding; +import dev.isxander.controlify.bindings.input.InputType; import dev.isxander.controlify.config.settings.device.DeviceSettings; import dev.isxander.controlify.config.settings.profile.InputSettings; import dev.isxander.controlify.controller.*; @@ -84,7 +85,24 @@ public void pushState(ControllerState state) { this.stateNow = state; this.updateDeadzoneView(); + // Get all the CompoundInputs from the current inputs. + List sortedBindings = this.inputBindings.values().stream() + .filter(object -> object.boundInput().type() == InputType.COMPOUND) + .toList(); + List boundIdentifiers = new ArrayList<>(); + // For all the active CompoundInputs, get a list of the underlying active inputs. + sortedBindings.forEach(inputBinding -> { + if (inputBinding.boundInput().state(this.deadzoneStateNow) == 1) + boundIdentifiers.addAll(inputBinding.boundInput().getRelevantInputs()); + }); + Set boundIdentifierSet = new HashSet<>(boundIdentifiers); + // Prevent those active inputs from being used in a non-compound bind. for (InputBinding binding : this.inputBindings.values()) { + if (binding.boundInput().type() != InputType.COMPOUND) { + if (boundIdentifierSet.containsAll(binding.boundInput().getRelevantInputs())) { + binding.limitActivity(); + } + } binding.pushState(this.deadzoneStateNow); } } diff --git a/src/main/java/dev/isxander/controlify/gui/controllers/BindController.java b/src/main/java/dev/isxander/controlify/gui/controllers/BindController.java index a5c6abff0..828ded679 100644 --- a/src/main/java/dev/isxander/controlify/gui/controllers/BindController.java +++ b/src/main/java/dev/isxander/controlify/gui/controllers/BindController.java @@ -17,7 +17,6 @@ import dev.isxander.yacl3.gui.YACLScreen; import dev.isxander.yacl3.gui.controllers.ControllerWidget; import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; @@ -25,7 +24,8 @@ import net.minecraft.resources.Identifier; import org.lwjgl.glfw.GLFW; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; public class BindController implements Controller { private final Option option; @@ -106,7 +106,7 @@ public boolean mouseClicked(MouseButtonEvent mouseButtonEvent, boolean doubleCli private void openConsumerScreen() { awaitingControllerInput = true; - MinecraftUtil.setScreen(new BindConsumerScreen(this::getPressedBind, control.option(), this, MinecraftUtil.getScreen())); + MinecraftUtil.setScreen(new BindConsumerScreen(this::getPressedBinds, control.option(), this, MinecraftUtil.getScreen())); } @Override @@ -141,31 +141,37 @@ protected int getValueColor() { return control.conflicting ? 0xFFFF5555 : super.getValueColor(); } - public Optional getPressedBind() { + public List getPressedBinds() { InputComponent input = control.controller.input().orElseThrow(); ControllerStateView state = input.stateNow(); ControllerStateView prevState = input.stateThen(); + List pressedBinds = new ArrayList<>(); for (Identifier button : state.getButtons()) { if (state.isButtonDown(button) && !prevState.isButtonDown(button)) { - return Optional.of(new ButtonInput(button)); + pressedBinds.add(new ButtonInput(button)); + } else if (!state.isButtonDown(button) && prevState.isButtonDown(button)) { + return List.of(EmptyInput.INSTANCE); } } for (Identifier axis : state.getAxes()) { if (state.getAxisState(axis) > 0.5f && prevState.getAxisState(axis) <= 0.5f) { - return Optional.of(new AxisInput(axis)); + pressedBinds.add(new AxisInput(axis)); + } else if (state.getAxisState(axis) <= 0.5f && prevState.getAxisState(axis) > 0.5f) { + return List.of(EmptyInput.INSTANCE); } } for (Identifier hat : state.getHats()) { HatState hatState = state.getHatState(hat); if (hatState != HatState.CENTERED && prevState.getHatState(hat) == HatState.CENTERED) { - return Optional.of(new HatInput(hat, hatState)); + pressedBinds.add(new HatInput(hat, hatState)); + } else if (hatState == HatState.CENTERED && prevState.getHatState(hat) != HatState.CENTERED) { + return List.of(EmptyInput.INSTANCE); } } - - return Optional.empty(); + return pressedBinds; } } } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java index 28d7d3305..5f0e25db2 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java @@ -1,6 +1,9 @@ package dev.isxander.controlify.gui.screen; +import dev.isxander.controlify.bindings.input.CompoundInput; +import dev.isxander.controlify.bindings.input.EmptyInput; import dev.isxander.controlify.bindings.input.Input; +import dev.isxander.controlify.bindings.input.InputType; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.gui.controllers.BindController; import dev.isxander.controlify.screenop.ScreenProcessor; @@ -15,7 +18,8 @@ import net.minecraft.network.chat.Component; import org.jspecify.annotations.NonNull; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; public class BindConsumerScreen extends Screen implements ScreenProcessorProvider { private final BindConsumer bindConsumer; @@ -23,6 +27,7 @@ public class BindConsumerScreen extends Screen implements ScreenProcessorProvide private final Screen backgroundScreen; private final BindController.BindControllerElement widgetToFocus; private final ScreenProcessorImpl screenProcessor = new ScreenProcessorImpl(this); + private final List currentInputs = new ArrayList<>(); private int ticksTillClose; private int ticksTillInput; @@ -77,11 +82,28 @@ public void tick() { } // tick runs after all controller input ticks - - Optional pressedBind = bindConsumer.getPressedBind(); - if (pressedBind.isPresent()) { - option.requestSet(pressedBind.get()); - returnToBackground(); + List pressedBinds = bindConsumer.getPressedBinds(); + if (!pressedBinds.isEmpty()) { + if (pressedBinds.getFirst().type() == InputType.EMPTY) { // Create bind if input released + if (currentInputs.size() == 1) { + option.requestSet(currentInputs.getFirst()); + } else if (currentInputs.size() > 1) { + option.requestSet(new CompoundInput(currentInputs)); + } else { + option.requestSet(EmptyInput.INSTANCE); + } + returnToBackground(); + } else { + List pressedBindCopy = currentInputs.stream().toList(); + // Input validation + // This is done because sometimes it's possible to input a button twice. + for (Input input : currentInputs) { + if (pressedBindCopy.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) { + currentInputs.add(input); + } + } + //pressedBind.ifPresent(currentInputs::addAll); + } } } @@ -135,7 +157,10 @@ public ScreenProcessor screenProcessor() { } public interface BindConsumer { - Optional getPressedBind(); + /** + * @return A list of all newly pressed binds during this check. Or a list containing only {@link EmptyInput} to indicate that the user has released an input. + */ + List getPressedBinds(); } private static class ScreenProcessorImpl extends ScreenProcessor { From df137df3413832788a5cacef862a421a88826a86 Mon Sep 17 00:00:00 2001 From: TotallyDem Date: Fri, 3 Jul 2026 03:24:11 -0700 Subject: [PATCH 2/3] FIxed last minute input validation code --- .../dev/isxander/controlify/gui/screen/BindConsumerScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java index 5f0e25db2..1efecf537 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java @@ -97,7 +97,7 @@ public void tick() { List pressedBindCopy = currentInputs.stream().toList(); // Input validation // This is done because sometimes it's possible to input a button twice. - for (Input input : currentInputs) { + for (Input input : pressedBinds) { if (pressedBindCopy.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) { currentInputs.add(input); } From 39f9cf275070e46c8b305ae717ca52c5c19106f9 Mon Sep 17 00:00:00 2001 From: TotallyDem Date: Fri, 3 Jul 2026 13:19:03 -0700 Subject: [PATCH 3/3] Removed currentInputs deep copy from BindConsumerScreen. Wasn't necessary since the list wasn't modified mid-iteration. --- .../isxander/controlify/gui/screen/BindConsumerScreen.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java index 1efecf537..59164515f 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java @@ -94,15 +94,13 @@ public void tick() { } returnToBackground(); } else { - List pressedBindCopy = currentInputs.stream().toList(); // Input validation // This is done because sometimes it's possible to input a button twice. for (Input input : pressedBinds) { - if (pressedBindCopy.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) { + if (currentInputs.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) { currentInputs.add(input); } } - //pressedBind.ifPresent(currentInputs::addAll); } } }