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..59164515f 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,26 @@ 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 {
+ // Input validation
+ // This is done because sometimes it's possible to input a button twice.
+ for (Input input : pressedBinds) {
+ if (currentInputs.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) {
+ currentInputs.add(input);
+ }
+ }
+ }
}
}
@@ -135,7 +155,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 {