Skip to content

Commit 26d797d

Browse files
committed
Adding multiple input support
It's currently a little rough around the edges. Notably, the icons for + between inputs in the UI are missing.
1 parent 98da241 commit 26d797d

7 files changed

Lines changed: 117 additions & 17 deletions

File tree

src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dev.isxander.controlify.bindings.BindContext;
44
import dev.isxander.controlify.bindings.StateAccess;
5+
import dev.isxander.controlify.bindings.input.CompoundInput;
56
import dev.isxander.controlify.bindings.input.EmptyInput;
67
import dev.isxander.controlify.bindings.input.Input;
78
import dev.isxander.controlify.bindings.output.AnalogueOutput;
@@ -100,6 +101,13 @@ default Component inputIcon() {
100101
*/
101102
void fakePress();
102103

104+
/**
105+
* Prevents this binding from being used. Used every state update where the bind is meant to be suppressed.
106+
* This is used to prevent individual bindings from being activated when a {@link CompoundInput} is being used.
107+
* Does not prevent {@link #fakePress()} from being used.
108+
*/
109+
void limitActivity();
110+
103111
/**
104112
* Set the input that this binding is bound to.
105113
*

src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ public void fakePress() {
178178
fakePressState = 0;
179179
}
180180

181+
@Override
182+
public void limitActivity() {
183+
if (fakePressState == -1)
184+
fakePressState = 3;
185+
}
186+
181187
@Override
182188
public void setBoundInput(Input input) {
183189
if (inputComponent != null) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dev.isxander.controlify.bindings.input;
2+
3+
import com.mojang.serialization.Codec;
4+
import com.mojang.serialization.MapCodec;
5+
import com.mojang.serialization.codecs.RecordCodecBuilder;
6+
import dev.isxander.controlify.controller.input.ControllerStateView;
7+
import net.minecraft.resources.Identifier;
8+
9+
import java.util.List;
10+
11+
public record CompoundInput(List<Input> inputs) implements Input {
12+
public static final String INPUT_ID = "compound";
13+
14+
public static final MapCodec<CompoundInput> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
15+
Codec.lazyInitialized(()-> Input.CODEC).listOf().fieldOf(INPUT_ID).forGetter(CompoundInput::inputs)
16+
).apply(instance, CompoundInput::new));
17+
18+
19+
@Override
20+
public float state(ControllerStateView state) {
21+
for (Input input : inputs) {
22+
if (input.state(state) == 0) return 0;
23+
}
24+
return 1;
25+
}
26+
27+
@Override
28+
public List<Identifier> getRelevantInputs() {
29+
return inputs.stream().flatMap(input -> input.getRelevantInputs().stream()).toList();
30+
}
31+
32+
@Override
33+
public InputType<?> type() {
34+
return InputType.COMPOUND;
35+
}
36+
}

src/main/java/dev/isxander/controlify/bindings/input/InputType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ public record InputType<T extends Input>(String id, MapCodec<T> codec) implement
1919
public static final InputType<ButtonInput> BUTTON = new InputType<>(ButtonInput.INPUT_ID, ButtonInput.CODEC);
2020
public static final InputType<AxisInput> AXIS = new InputType<>(AxisInput.INPUT_ID, AxisInput.CODEC);
2121
public static final InputType<HatInput> HAT = new InputType<>(HatInput.INPUT_ID, HatInput.CODEC);
22+
public static final InputType<CompoundInput> COMPOUND = new InputType<CompoundInput>(CompoundInput.INPUT_ID, CompoundInput.CODEC);
2223
public static final InputType<EmptyInput> EMPTY = new InputType<>(EmptyInput.INPUT_ID, EmptyInput.CODEC);
2324

2425
public static final InputType<?>[] TYPES = {
25-
InputType.BUTTON, InputType.AXIS, InputType.HAT, InputType.EMPTY
26+
InputType.BUTTON, InputType.AXIS, InputType.HAT, InputType.COMPOUND, InputType.EMPTY
2627
};
2728

2829
public static <T extends StringRepresentable, E> MapCodec<E> createCodec(

src/main/java/dev/isxander/controlify/controller/input/InputComponent.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dev.isxander.controlify.Controlify;
44
import dev.isxander.controlify.bindings.ControlifyBindApiImpl;
55
import dev.isxander.controlify.api.bind.InputBinding;
6+
import dev.isxander.controlify.bindings.input.InputType;
67
import dev.isxander.controlify.config.settings.device.DeviceSettings;
78
import dev.isxander.controlify.config.settings.profile.InputSettings;
89
import dev.isxander.controlify.controller.*;
@@ -84,7 +85,24 @@ public void pushState(ControllerState state) {
8485
this.stateNow = state;
8586
this.updateDeadzoneView();
8687

88+
// Get all the CompoundInputs from the current inputs.
89+
List<InputBinding> sortedBindings = this.inputBindings.values().stream()
90+
.filter(object -> object.boundInput().type() == InputType.COMPOUND)
91+
.toList();
92+
List<Identifier> boundIdentifiers = new ArrayList<>();
93+
// For all the active CompoundInputs, get a list of the underlying active inputs.
94+
sortedBindings.forEach(inputBinding -> {
95+
if (inputBinding.boundInput().state(this.deadzoneStateNow) == 1)
96+
boundIdentifiers.addAll(inputBinding.boundInput().getRelevantInputs());
97+
});
98+
Set<Identifier> boundIdentifierSet = new HashSet<>(boundIdentifiers);
99+
// Prevent those active inputs from being used in a non-compound bind.
87100
for (InputBinding binding : this.inputBindings.values()) {
101+
if (binding.boundInput().type() != InputType.COMPOUND) {
102+
if (boundIdentifierSet.containsAll(binding.boundInput().getRelevantInputs())) {
103+
binding.limitActivity();
104+
}
105+
}
88106
binding.pushState(this.deadzoneStateNow);
89107
}
90108
}

src/main/java/dev/isxander/controlify/gui/controllers/BindController.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
import dev.isxander.yacl3.gui.YACLScreen;
1818
import dev.isxander.yacl3.gui.controllers.ControllerWidget;
1919
import net.minecraft.ChatFormatting;
20-
import net.minecraft.client.Minecraft;
2120
import net.minecraft.client.gui.GuiGraphicsExtractor;
2221
import net.minecraft.client.input.KeyEvent;
2322
import net.minecraft.client.input.MouseButtonEvent;
2423
import net.minecraft.network.chat.Component;
2524
import net.minecraft.resources.Identifier;
2625
import org.lwjgl.glfw.GLFW;
2726

28-
import java.util.Optional;
27+
import java.util.ArrayList;
28+
import java.util.List;
2929

3030
public class BindController implements Controller<Input> {
3131
private final Option<Input> option;
@@ -106,7 +106,7 @@ public boolean mouseClicked(MouseButtonEvent mouseButtonEvent, boolean doubleCli
106106

107107
private void openConsumerScreen() {
108108
awaitingControllerInput = true;
109-
MinecraftUtil.setScreen(new BindConsumerScreen(this::getPressedBind, control.option(), this, MinecraftUtil.getScreen()));
109+
MinecraftUtil.setScreen(new BindConsumerScreen(this::getPressedBinds, control.option(), this, MinecraftUtil.getScreen()));
110110
}
111111

112112
@Override
@@ -141,31 +141,37 @@ protected int getValueColor() {
141141
return control.conflicting ? 0xFFFF5555 : super.getValueColor();
142142
}
143143

144-
public Optional<Input> getPressedBind() {
144+
public List<Input> getPressedBinds() {
145145
InputComponent input = control.controller.input().orElseThrow();
146146
ControllerStateView state = input.stateNow();
147147
ControllerStateView prevState = input.stateThen();
148+
List<Input> pressedBinds = new ArrayList<>();
148149

149150
for (Identifier button : state.getButtons()) {
150151
if (state.isButtonDown(button) && !prevState.isButtonDown(button)) {
151-
return Optional.of(new ButtonInput(button));
152+
pressedBinds.add(new ButtonInput(button));
153+
} else if (!state.isButtonDown(button) && prevState.isButtonDown(button)) {
154+
return List.of(EmptyInput.INSTANCE);
152155
}
153156
}
154157

155158
for (Identifier axis : state.getAxes()) {
156159
if (state.getAxisState(axis) > 0.5f && prevState.getAxisState(axis) <= 0.5f) {
157-
return Optional.of(new AxisInput(axis));
160+
pressedBinds.add(new AxisInput(axis));
161+
} else if (state.getAxisState(axis) <= 0.5f && prevState.getAxisState(axis) > 0.5f) {
162+
return List.of(EmptyInput.INSTANCE);
158163
}
159164
}
160165

161166
for (Identifier hat : state.getHats()) {
162167
HatState hatState = state.getHatState(hat);
163168
if (hatState != HatState.CENTERED && prevState.getHatState(hat) == HatState.CENTERED) {
164-
return Optional.of(new HatInput(hat, hatState));
169+
pressedBinds.add(new HatInput(hat, hatState));
170+
} else if (hatState == HatState.CENTERED && prevState.getHatState(hat) != HatState.CENTERED) {
171+
return List.of(EmptyInput.INSTANCE);
165172
}
166173
}
167-
168-
return Optional.empty();
174+
return pressedBinds;
169175
}
170176
}
171177
}

src/main/java/dev/isxander/controlify/gui/screen/BindConsumerScreen.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package dev.isxander.controlify.gui.screen;
22

3+
import dev.isxander.controlify.bindings.input.CompoundInput;
4+
import dev.isxander.controlify.bindings.input.EmptyInput;
35
import dev.isxander.controlify.bindings.input.Input;
6+
import dev.isxander.controlify.bindings.input.InputType;
47
import dev.isxander.controlify.controller.ControllerEntity;
58
import dev.isxander.controlify.gui.controllers.BindController;
69
import dev.isxander.controlify.screenop.ScreenProcessor;
@@ -15,14 +18,16 @@
1518
import net.minecraft.network.chat.Component;
1619
import org.jspecify.annotations.NonNull;
1720

18-
import java.util.Optional;
21+
import java.util.ArrayList;
22+
import java.util.List;
1923

2024
public class BindConsumerScreen extends Screen implements ScreenProcessorProvider {
2125
private final BindConsumer bindConsumer;
2226
private final Option<Input> option;
2327
private final Screen backgroundScreen;
2428
private final BindController.BindControllerElement widgetToFocus;
2529
private final ScreenProcessorImpl screenProcessor = new ScreenProcessorImpl(this);
30+
private final List<Input> currentInputs = new ArrayList<>();
2631

2732
private int ticksTillClose;
2833
private int ticksTillInput;
@@ -77,11 +82,28 @@ public void tick() {
7782
}
7883

7984
// tick runs after all controller input ticks
80-
81-
Optional<Input> pressedBind = bindConsumer.getPressedBind();
82-
if (pressedBind.isPresent()) {
83-
option.requestSet(pressedBind.get());
84-
returnToBackground();
85+
List<Input> pressedBinds = bindConsumer.getPressedBinds();
86+
if (!pressedBinds.isEmpty()) {
87+
if (pressedBinds.getFirst().type() == InputType.EMPTY) { // Create bind if input released
88+
if (currentInputs.size() == 1) {
89+
option.requestSet(currentInputs.getFirst());
90+
} else if (currentInputs.size() > 1) {
91+
option.requestSet(new CompoundInput(currentInputs));
92+
} else {
93+
option.requestSet(EmptyInput.INSTANCE);
94+
}
95+
returnToBackground();
96+
} else {
97+
List<Input> pressedBindCopy = currentInputs.stream().toList();
98+
// Input validation
99+
// This is done because sometimes it's possible to input a button twice.
100+
for (Input input : currentInputs) {
101+
if (pressedBindCopy.stream().noneMatch(storedInput -> storedInput.getRelevantInputs().containsAll(input.getRelevantInputs()))) {
102+
currentInputs.add(input);
103+
}
104+
}
105+
//pressedBind.ifPresent(currentInputs::addAll);
106+
}
85107
}
86108
}
87109

@@ -135,7 +157,10 @@ public ScreenProcessor<?> screenProcessor() {
135157
}
136158

137159
public interface BindConsumer {
138-
Optional<Input> getPressedBind();
160+
/**
161+
* @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.
162+
*/
163+
List<Input> getPressedBinds();
139164
}
140165

141166
private static class ScreenProcessorImpl extends ScreenProcessor<BindConsumerScreen> {

0 commit comments

Comments
 (0)