Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ eclipse
/neoforge/runs/
/neoforge/run/
/forge/
neoforge/logs/latest.log
common/src/main/java/com/mrcrayfish/controllable/mixin/client/MinecraftMixin.java
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
plugins {
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
id 'net.neoforged.moddev' version '2.0.73' apply false
}
repositories {
maven { url 'https://maven.mrcrayfish.com/' }
mavenCentral()
maven { url 'https://maven.blamejared.com' }
maven { url 'https://maven.terraformersmc.com/' }
maven { url 'https://maven.shedaniel.me' }
maven { url 'https://libraries.minecraft.net' }
maven { url 'https://maven.neoforged.net/releases' }
}
5 changes: 3 additions & 2 deletions common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ neoForge {
}

dependencies {
compileOnly "com.mrcrayfish:framework-common:${minecraft_version}-${framework_version}"
compileOnly files('libs/framework-common-1.21.1-0.13.11.jar')
compileOnly "mezz.jei:jei-${minecraft_version}-common:${jei_version}"
compileOnly "mezz.jei:jei-${minecraft_version}-gui:${jei_version}"
compileOnly "mezz.jei:jei-${minecraft_version}-lib:${jei_version}"
compileOnly "com.mrcrayfish:controllable-sdl:${controllable_sdl_version}"
compileOnly files('libs/controllable-sdl-2.32.10-1.1.0.jar')
compileOnly files('libs/ShoulderSurfing-NeoForge-1.21.1-4.16.1.jar')
compileOnly "dev.emi:emi-xplat-mojmap:${emi_version}"
compileOnly "me.shedaniel:RoughlyEnoughItems-neoforge:${rei_version}"
compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.7'
Expand Down
Binary file added common/libs/controllable-sdl-2.32.10-1.1.0.jar
Binary file not shown.
Binary file added common/libs/framework-common-1.21.1-0.13.11.jar
Binary file not shown.
13 changes: 13 additions & 0 deletions common/src/main/java/com/mrcrayfish/controllable/Controllable.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class Controllable
private static final boolean EMI_LOADED = Utils.isModLoaded("emi");
private static final boolean REI_LOADED = Utils.isModLoaded("roughlyenoughitems");
private static final boolean JEI_LOADED = Utils.isModLoaded("jei") && !EMI_LOADED && !REI_LOADED;
private static final boolean TACZ_LOADED = Utils.isModLoaded("tacz");

public static void init()
{
Expand All @@ -44,6 +45,13 @@ public static void init()
CAMERA_HANDLER.registerEvents();
RADIAL_MENU.registerEvents();
SCROLLING_HANDLER.registerEvents();

// DEBUG: Print all registered bindings
System.out.println("[Controllable] === Registered Bindings ===");
BINDING_REGISTRY.getBindings().forEach(binding -> {
System.out.println("[Controllable] " + binding.getDescription() + " -> Button: " + binding.getButton() + " (Multi: " + binding.isMultiButton() + ")");
});
System.out.println("[Controllable] === End Bindings ===");
}

public static BindingRegistry getBindingRegistry()
Expand Down Expand Up @@ -76,6 +84,11 @@ public static RumbleHandler getRumbleHandler()
return RUMBLE_HANDLER;
}

public static boolean isTaczLoaded()
{
return TACZ_LOADED;
}

public static boolean isArchitecturyLoaded()
{
return ARCHITECTURY_LOADED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public static void init()
private static void onScreenInit(Screen screen)
{
ButtonBinding.resetButtonStates();
// Clear all active tick/render/movement handlers and combo suppression state so that
// held-button actions (e.g. jump from a combo) don't persist after a screen opens.
Controllable.getInputHandler().clearActiveHandlers();

// Fixes an issue where using item is not stopped after opening a screen
if(!released)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.mrcrayfish.controllable.client.gui.navigation.SlotNavigationPoint;
import com.mrcrayfish.controllable.client.gui.navigation.WidgetNavigationPoint;
import com.mrcrayfish.controllable.client.input.Controller;
import com.mrcrayfish.controllable.client.input.ButtonStates;
import com.mrcrayfish.controllable.client.settings.AnalogMovement;
import com.mrcrayfish.controllable.client.settings.Thumbstick;
import com.mrcrayfish.controllable.client.util.ClientHelper;
Expand Down Expand Up @@ -82,6 +83,7 @@

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand All @@ -101,6 +103,20 @@ public class InputHandler
private @Nullable ButtonBinding activeVirtualBinding;
private boolean initialized;

/**
* Buttons that appear as non-sole members in at least one multi-button combo.
* These are "pure modifier" buttons — pressing them must never immediately fire their
* single-button binding because doing so would conflict with any combo using them.
* Rebuilt every time the binding registry cache is rebuilt via rebuildComboModifiers().
*/
private final Set<Integer> comboModifierButtons = new HashSet<>();

/**
* Tracks which buttons were consumed as part of a successfully fired multi-button combo so
* that on release we do NOT fire their single-button bindings.
*/
private final Set<Integer> comboSuppressedButtons = new HashSet<>();

@ApiStatus.Internal
public InputHandler()
{
Expand Down Expand Up @@ -128,48 +144,154 @@ public ButtonBinding getActiveVirtualBinding()
return this.activeVirtualBinding;
}

/**
* Called by InputProcessor with the full set of buttons newly pressed in a single frame.
* Handling all pressed buttons together allows us to detect combos whose buttons were all
* pressed within the same poll frame (e.g. LB+RB+Y captured together).
*/
@ApiStatus.Internal
public void handleButtonInput(Controller controller, int button, boolean state)
public void handleButtonsPressed(Controller controller, List<Integer> pressedButtons)
{
if(controller == null)
if(controller == null || pressedButtons.isEmpty())
return;

controller.updateInputTime();

if(state)
// Pass 1: find every multi-button combo fully satisfied in this frame.
// A combo is satisfied if ALL its buttons are currently tracked as held,
// and at least one of its buttons was newly pressed this frame.
ButtonStates tracked = controller.getTrackedButtonStates();
List<ButtonBinding> completeCombos = new ArrayList<>();
Set<ButtonBinding> seen = new HashSet<>();

for(int button : pressedButtons)
{
for(ButtonBinding binding : Controllable.getBindingRegistry().getBindingsForButton(button))
{
if(!binding.isMultiButton() || !seen.add(binding))
continue;

boolean hasNewButton = false;
boolean allHeld = true;
for(int required : binding.getButtons())
{
if(!tracked.getState(required)) { allHeld = false; break; }
if(pressedButtons.contains(required)) hasNewButton = true;
}
if(allHeld && hasNewButton)
completeCombos.add(binding);
}
}

// Pass 2: among complete combos, only fire the longest ones.
// Shorter combos that are strict subsets of a longer one are suppressed.
if(!completeCombos.isEmpty())
{
int maxLen = completeCombos.stream().mapToInt(ButtonBinding::getButtonCount).max().orElse(0);
for(ButtonBinding binding : completeCombos)
{
if(binding.getButtonCount() < maxLen)
continue;
if(this.handleBindingPressed(controller, binding, false))
break;
{
for(int comboBtn : binding.getButtons())
this.comboSuppressedButtons.add(comboBtn);
}
}
}
else

// Pass 3: fire single-button bindings for buttons not consumed by any combo.
for(int button : pressedButtons)
{
if(this.comboSuppressedButtons.contains(button))
continue;
if(this.comboModifierButtons.contains(button))
continue;

for(ButtonBinding binding : Controllable.getBindingRegistry().getBindingsForButton(button))
{
ButtonHandler handler = binding.getHandler();
if(!(handler instanceof BindingPressed))
if(binding.isMultiButton())
continue;
if(this.handleBindingPressed(controller, binding, false))
break;
}
}
}

if(!binding.isButtonDown())
continue;
/** Called for individual button releases — releases are always single-button events. */
@ApiStatus.Internal
public void handleButtonInput(Controller controller, int button, boolean state)
{
if(controller == null || state)
return;

ButtonBinding.setButtonState(binding, false);
controller.updateInputTime();
this.handleButtonReleased(controller, button);
}
/**
* Called when a physical button is released.
*
* 1. Release any active multi-button combos that include this button (combo is broken).
* 2. Clear combo-suppression for this button.
* 3. If the button is a known modifier but was NOT combo-suppressed, fire its single-button
* binding now as an instant tap (the user pressed and released a modifier alone).
* 4. Release any active single-button binding normally.
*/
private void handleButtonReleased(Controller controller, int button)
{
Minecraft mc = Minecraft.getInstance();

if(!(handler instanceof BindingReleased released))
continue;
// --- Release active multi-button combos whose combination is now broken ---
for(ButtonBinding binding : Controllable.getBindingRegistry().getBindingsForButton(button))
{
if(!binding.isMultiButton())
continue;

// Cancel the handler if context is no longer valid
if(!binding.getContext().isActive())
continue;
ButtonHandler handler = binding.getHandler();
if(!(handler instanceof BindingPressed))
continue;

if(!binding.isButtonDown())
continue;

Minecraft mc = Minecraft.getInstance();
ButtonBinding.setButtonState(binding, false);

if(handler instanceof BindingReleased released && binding.getContext().isActive())
{
Context context = new Context(binding, controller, mc, mc.player, mc.level, mc.screen, false);
released.handleReleased(context);
return;
}
}

// --- Clear combo suppression for this button ---
boolean wasComboSuppressed = this.comboSuppressedButtons.remove(button);

// --- Release normal single-button bindings ---
// Skip if this button was part of a completed combo this session.
if(wasComboSuppressed)
return;

for(ButtonBinding binding : Controllable.getBindingRegistry().getBindingsForButton(button))
{
if(binding.isMultiButton())
continue;

ButtonHandler handler = binding.getHandler();
if(!(handler instanceof BindingPressed))
continue;

if(!binding.isButtonDown())
continue;

ButtonBinding.setButtonState(binding, false);

if(handler instanceof BindingReleased released && binding.getContext().isActive())
{
Context context = new Context(binding, controller, mc, mc.player, mc.level, mc.screen, false);
released.handleReleased(context);
}
return;
}
}

@ApiStatus.Internal
Expand Down Expand Up @@ -769,11 +891,51 @@ public static void craftRecipeBookItem()
}
}

/**
* Rebuilds the set of pure combo modifier buttons from the current binding registry.
* A button is a "pure modifier" ONLY if it appears in at least one multi-button combo AND
* has NO single-button binding of its own. Buttons that have both a single binding and
* appear in combos are NOT pure modifiers — they fire their single binding normally and
* only get suppressed when a combo actually completes using them.
* Called by BindingRegistry.rebuildCache().
*/
@ApiStatus.Internal
public void rebuildComboModifiers()
{
this.comboModifierButtons.clear();

java.util.List<ButtonBinding> allBindings = Controllable.getBindingRegistry().getRegisteredBindings();

// Collect all buttons that have at least one single-button binding
Set<Integer> hasSingleBinding = new HashSet<>();
for(ButtonBinding binding : allBindings)
{
if(!binding.isMultiButton() && !binding.isUnbound())
{
hasSingleBinding.add(binding.getButton());
}
}

// A button is a pure modifier only if it appears in a combo but has NO single binding
for(ButtonBinding binding : allBindings)
{
if(binding.isMultiButton())
{
for(int btn : binding.getButtons())
{
if(!hasSingleBinding.contains(btn))
this.comboModifierButtons.add(btn);
}
}
}
}

public void clearActiveHandlers()
{
this.activeTickHandlers.clear();
this.activeRenderHandlers.clear();
this.activeMovementInputHandlers.clear();
this.comboSuppressedButtons.clear();
}

public enum Navigate
Expand Down
Loading