1
0
forked from Clones/Controlify

joystick support

This commit is contained in:
isXander
2023-02-16 12:25:55 +00:00
parent 1b5c9daf94
commit 5a1504df76
134 changed files with 2296 additions and 820 deletions

View File

@ -1,17 +1,18 @@
package dev.isxander.controlify;
import com.mojang.logging.LogUtils;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.config.ControlifyConfig;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.event.ControlifyEvents;
import dev.isxander.controlify.ingame.guide.InGameButtonGuide;
import dev.isxander.controlify.ingame.InGameInputHandler;
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
@ -27,7 +28,7 @@ public class Controlify {
public static final Logger LOGGER = LogUtils.getLogger();
private static Controlify instance = null;
private Controller currentController;
private Controller<?, ?> currentController;
private InGameInputHandler inGameInputHandler;
public InGameButtonGuide inGameButtonGuide;
private VirtualMouseHandler virtualMouseHandler;
@ -36,12 +37,15 @@ public class Controlify {
private final ControlifyConfig config = new ControlifyConfig();
private final Queue<Controller> calibrationQueue = new ArrayDeque<>();
private final Queue<Controller<?, ?>> calibrationQueue = new ArrayDeque<>();
public void initializeControllers() {
LOGGER.info("Discovering and initializing controllers...");
public void onInitializeInput() {
Minecraft minecraft = Minecraft.getInstance();
inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controllers
config().load();
controllerHIDService = new ControllerHIDService();
// find already connected controllers
@ -49,7 +53,7 @@ public class Controlify {
if (GLFW.glfwJoystickPresent(i)) {
int jid = i;
controllerHIDService.awaitNextController(device -> {
setCurrentController(Controller.create(jid, device));
setCurrentController(Controller.createOrGet(jid, device));
LOGGER.info("Controller found: " + currentController.name());
if (!config().loadOrCreateControllerData(currentController)) {
@ -61,13 +65,11 @@ public class Controlify {
controllerHIDService.start();
config().load();
// listen for new controllers
GLFW.glfwSetJoystickCallback((jid, event) -> {
if (event == GLFW.GLFW_CONNECTED) {
controllerHIDService.awaitNextController(device -> {
setCurrentController(Controller.create(jid, device));
setCurrentController(Controller.createOrGet(jid, device));
LOGGER.info("Controller connected: " + currentController.name());
this.setCurrentInputMode(InputMode.CONTROLLER);
@ -82,11 +84,10 @@ public class Controlify {
Component.translatable("controlify.toast.controller_connected.description", currentController.name())
));
});
} else if (event == GLFW.GLFW_DISCONNECTED) {
var controller = Controller.CONTROLLERS.remove(jid);
if (controller != null) {
setCurrentController(Controller.CONTROLLERS.values().stream().filter(Controller::connected).findFirst().orElse(null));
setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
LOGGER.info("Controller disconnected: " + controller.name());
this.setCurrentInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
@ -100,11 +101,14 @@ public class Controlify {
}
});
this.virtualMouseHandler = new VirtualMouseHandler();
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
}
public void initializeControlify() {
this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller
this.virtualMouseHandler = new VirtualMouseHandler();
}
public void tick(Minecraft client) {
var minecraft = Minecraft.getInstance();
if (minecraft.getOverlay() == null) {
@ -124,7 +128,7 @@ public class Controlify {
}
}
for (Controller controller : Controller.CONTROLLERS.values()) {
for (var controller : Controller.CONTROLLERS.values()) {
controller.updateState();
}
@ -154,11 +158,11 @@ public class Controlify {
return config;
}
public Controller currentController() {
public Controller<?, ?> currentController() {
return currentController;
}
public void setCurrentController(Controller controller) {
public void setCurrentController(Controller<?, ?> controller) {
if (this.currentController == controller) return;
this.currentController = controller;

View File

@ -1,91 +0,0 @@
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 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_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
DPAD_UP(state -> state.buttons().dpadUp(), "dpad_up"),
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(), "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"),
NONE((state, controller) -> 0f, "none");
private final BiFunction<ControllerState, Controller, Float> state;
private final String identifier;
Bind(BiFunction<ControllerState, Controller, Float> state, String identifier) {
this.state = state;
this.identifier = identifier;
}
Bind(Function<ControllerState, Boolean> state, String identifier) {
this((state1, controller) -> state.apply(state1) ? 1f : 0f, identifier);
}
@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) {
if (this != NONE)
ButtonRenderer.drawButton(this, controller, matrices, x, centerY);
}
@Override
public ButtonRenderer.DrawSize drawSize() {
if (this == NONE) return new ButtonRenderer.DrawSize(0, 0);
return new ButtonRenderer.DrawSize(22, 22);
}
public String identifier() {
return identifier;
}
public ResourceLocation textureLocation(Controller controller) {
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;
}
return null;
}
}

View File

@ -1,8 +1,9 @@
package dev.isxander.controlify.bindings;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
@FunctionalInterface
public interface BindingSupplier {
ControllerBinding get(Controller controller);
public interface BindingSupplier<T extends ControllerState> {
ControllerBinding<T> get(Controller<T, ?> controller);
}

View File

@ -1,78 +0,0 @@
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<Bind> binds;
CompoundBind(Bind... binds) {
this.binds = new LinkedHashSet<>(Arrays.asList(binds));
if (this.binds.contains(Bind.NONE)) throw new IllegalArgumentException("Cannot have NONE in a compound bind!");
}
public Set<Bind> 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);
}
}

View File

@ -2,6 +2,7 @@ package dev.isxander.controlify.bindings;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import net.minecraft.client.KeyMapping;
import net.minecraft.locale.Language;
import net.minecraft.network.chat.Component;
@ -13,17 +14,17 @@ import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
public class ControllerBinding {
private final Controller controller;
private IBind bind;
private final IBind defaultBind;
public class ControllerBinding<T extends ControllerState> {
private final Controller<T, ?> controller;
private IBind<T> bind;
private final IBind<T> defaultBind;
private final ResourceLocation id;
private final Component name, description;
private final KeyMappingOverride override;
private static final Map<Controller, Set<Bind>> pressedBinds = new HashMap<>();
private static final Map<Controller<?, ?>, Set<IBind<?>>> pressedBinds = new HashMap<>();
public ControllerBinding(Controller controller, IBind defaultBind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride) {
public ControllerBinding(Controller<T, ?> controller, IBind<T> defaultBind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride) {
this.controller = controller;
this.bind = this.defaultBind = defaultBind;
this.id = id;
@ -33,22 +34,38 @@ public class ControllerBinding {
this.override = override != null ? new KeyMappingOverride(override, toggleOverride) : null;
}
public ControllerBinding(Controller controller, IBind defaultBind, ResourceLocation id) {
public ControllerBinding(Controller<T, ?> controller, IBind<T> defaultBind, ResourceLocation id) {
this(controller, defaultBind, id, null, () -> false);
}
public ControllerBinding(Controller<T, ?> controller, GamepadBind defaultBind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride) {
this(controller, controller instanceof GamepadController ? (IBind<T>) defaultBind : new EmptyBind<>(), id, override, toggleOverride);
}
public ControllerBinding(Controller<T, ?> controller, GamepadBind defaultBind, ResourceLocation id) {
this(controller, defaultBind, id, null, () -> false);
}
public float state() {
return bind.state(controller.state(), controller);
return bind.state(controller.state());
}
public float prevState() {
return bind.state(controller.prevState());
}
public boolean held() {
return bind.held(controller.state(), controller);
}
public boolean prevHeld() {
return bind.held(controller.prevState(), controller);
}
public boolean justPressed() {
if (hasBindPressed(this)) return false;
if (held() && !bind.held(controller.prevState(), controller)) {
if (held() && !prevHeld()) {
addPressedBind(this);
return true;
} else {
@ -59,7 +76,7 @@ public class ControllerBinding {
public boolean justReleased() {
if (hasBindPressed(this)) return false;
if (!held() && bind.held(controller.prevState(), controller)) {
if (!held() && prevHeld()) {
addPressedBind(this);
return true;
} else {
@ -67,15 +84,15 @@ public class ControllerBinding {
}
}
public IBind currentBind() {
public IBind<T> currentBind() {
return bind;
}
public void setCurrentBind(IBind bind) {
public void setCurrentBind(IBind<T> bind) {
this.bind = bind;
}
public IBind defaultBind() {
public IBind<T> defaultBind() {
return defaultBind;
}
@ -97,27 +114,23 @@ public class ControllerBinding {
// FIXME: very hack solution please remove me
public static void clearPressedBinds(Controller controller) {
public static void clearPressedBinds(Controller<?, ?> controller) {
if (pressedBinds.containsKey(controller)) {
pressedBinds.get(controller).clear();
}
}
private static boolean hasBindPressed(ControllerBinding binding) {
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) {
private static void addPressedBind(ControllerBinding<?> binding) {
pressedBinds.computeIfAbsent(binding.controller, c -> new HashSet<>()).addAll(getBinds(binding.bind));
}
private static Set<Bind> getBinds(IBind bind) {
if (bind instanceof CompoundBind compoundBind) {
return compoundBind.binds();
} else {
return Set.of((Bind) bind);
}
private static Set<IBind<?>> getBinds(IBind<?> bind) {
return Set.of(bind);
}
public record KeyMappingOverride(KeyMapping keyMapping, BooleanSupplier toggleable) {

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonObject;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.event.ControlifyEvents;
import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor;
import net.minecraft.client.KeyMapping;
@ -12,9 +13,10 @@ import net.minecraft.resources.ResourceLocation;
import java.util.*;
public class ControllerBindings {
public final ControllerBinding
public class ControllerBindings<T extends ControllerState> {
public final ControllerBinding<T>
WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT,
LOOK_UP, LOOK_DOWN, LOOK_LEFT, LOOK_RIGHT,
JUMP, SNEAK,
ATTACK, USE,
SPRINT,
@ -27,51 +29,71 @@ public class ControllerBindings {
OPEN_CHAT,
GUI_PRESS, GUI_BACK,
GUI_NEXT_TAB, GUI_PREV_TAB,
VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_SHIFT_CLICK, VMOUSE_SCROLL_UP, VMOUSE_SCROLL_DOWN, VMOUSE_ESCAPE, VMOUSE_SHIFT, VMOUSE_TOGGLE,
PICK_BLOCK,
TOGGLE_HUD_VISIBILITY,
SHOW_PLAYER_LIST;
SHOW_PLAYER_LIST,
VMOUSE_MOVE_UP, VMOUSE_MOVE_DOWN, VMOUSE_MOVE_LEFT, VMOUSE_MOVE_RIGHT,
VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_SHIFT_CLICK,
VMOUSE_SCROLL_UP, VMOUSE_SCROLL_DOWN,
VMOUSE_ESCAPE, VMOUSE_SHIFT,
VMOUSE_TOGGLE,
GUI_NAVI_UP, GUI_NAVI_DOWN, GUI_NAVI_LEFT, GUI_NAVI_RIGHT,
YACL_CYCLE_OPT_FORWARD, YACL_CYCLE_OPT_BACKWARD;
private final Map<ResourceLocation, ControllerBinding> registry = new LinkedHashMap<>();
private final Map<ResourceLocation, ControllerBinding<T>> registry = new LinkedHashMap<>();
private final Controller controller;
private final Controller<T, ?> controller;
public ControllerBindings(Controller controller) {
public ControllerBindings(Controller<T, ?> controller) {
this.controller = controller;
var options = Minecraft.getInstance().options;
register(WALK_FORWARD = new ControllerBinding(controller, Bind.LEFT_STICK_FORWARD, new ResourceLocation("controlify", "walk_forward")));
register(WALK_BACKWARD = new ControllerBinding(controller, Bind.LEFT_STICK_BACKWARD, new ResourceLocation("controlify", "walk_backward")));
register(WALK_LEFT = new ControllerBinding(controller, Bind.LEFT_STICK_LEFT, new ResourceLocation("controlify", "strafe_left")));
register(WALK_RIGHT = new ControllerBinding(controller, Bind.LEFT_STICK_RIGHT, new ResourceLocation("controlify", "strafe_right")));
register(JUMP = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "jump"), options.keyJump, () -> false));
register(SNEAK = new ControllerBinding(controller, Bind.RIGHT_STICK_PRESS, new ResourceLocation("controlify", "sneak"), options.keyShift, () -> controller.config().toggleSneak));
register(ATTACK = new ControllerBinding(controller, Bind.RIGHT_TRIGGER, new ResourceLocation("controlify", "attack"), options.keyAttack, () -> false));
register(USE = new ControllerBinding(controller, Bind.LEFT_TRIGGER, new ResourceLocation("controlify", "use"), options.keyUse, () -> false));
register(SPRINT = new ControllerBinding(controller, Bind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "sprint"), options.keySprint, () -> controller.config().toggleSprint));
register(DROP = new ControllerBinding(controller, Bind.DPAD_DOWN, new ResourceLocation("controlify", "drop"), options.keyDrop, () -> false));
register(NEXT_SLOT = new ControllerBinding(controller, Bind.RIGHT_BUMPER, new ResourceLocation("controlify", "next_slot")));
register(PREV_SLOT = new ControllerBinding(controller, Bind.LEFT_BUMPER, new ResourceLocation("controlify", "prev_slot")));
register(PAUSE = new ControllerBinding(controller, Bind.START, new ResourceLocation("controlify", "pause")));
register(INVENTORY = new ControllerBinding(controller, Bind.Y_BUTTON, new ResourceLocation("controlify", "inventory"), options.keyInventory, () -> false));
register(CHANGE_PERSPECTIVE = new ControllerBinding(controller, Bind.BACK, new ResourceLocation("controlify", "change_perspective"), options.keyTogglePerspective, () -> false));
register(SWAP_HANDS = new ControllerBinding(controller, Bind.X_BUTTON, new ResourceLocation("controlify", "swap_hands"), options.keySwapOffhand, () -> false));
register(OPEN_CHAT = new ControllerBinding(controller, Bind.DPAD_UP, new ResourceLocation("controlify", "open_chat"), options.keyChat, () -> false));
register(GUI_PRESS = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "gui_press")));
register(GUI_BACK = new ControllerBinding(controller, Bind.B_BUTTON, new ResourceLocation("controlify", "gui_back")));
register(GUI_NEXT_TAB = new ControllerBinding(controller, Bind.RIGHT_BUMPER, new ResourceLocation("controlify", "gui_next_tab")));
register(GUI_PREV_TAB = new ControllerBinding(controller, Bind.LEFT_BUMPER, new ResourceLocation("controlify", "gui_prev_tab")));
register(PICK_BLOCK = new ControllerBinding(controller, Bind.DPAD_LEFT, new ResourceLocation("controlify", "pick_block"), options.keyPickItem, () -> false));
register(TOGGLE_HUD_VISIBILITY = new ControllerBinding(controller, Bind.NONE, new ResourceLocation("controlify", "toggle_hud_visibility")));
register(SHOW_PLAYER_LIST = new ControllerBinding(controller, Bind.DPAD_RIGHT, new ResourceLocation("controlify", "show_player_list"), options.keyPlayerList, () -> false));
register(VMOUSE_LCLICK = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "vmouse_lclick")));
register(VMOUSE_RCLICK = new ControllerBinding(controller, Bind.X_BUTTON, new ResourceLocation("controlify", "vmouse_rclick")));
register(VMOUSE_SHIFT_CLICK = new ControllerBinding(controller, Bind.Y_BUTTON, new ResourceLocation("controlify", "vmouse_shift_click")));
register(VMOUSE_SCROLL_UP = new ControllerBinding(controller, Bind.RIGHT_STICK_FORWARD, new ResourceLocation("controlify", "vmouse_scroll_up")));
register(VMOUSE_SCROLL_DOWN = new ControllerBinding(controller, Bind.RIGHT_STICK_BACKWARD, new ResourceLocation("controlify", "vmouse_scroll_down")));
register(VMOUSE_ESCAPE = new ControllerBinding(controller, Bind.B_BUTTON, new ResourceLocation("controlify", "vmouse_escape")));
register(VMOUSE_SHIFT = new ControllerBinding(controller, Bind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "vmouse_shift")));
register(VMOUSE_TOGGLE = new ControllerBinding(controller, Bind.BACK, new ResourceLocation("controlify", "vmouse_toggle")));
register(WALK_FORWARD = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_FORWARD, new ResourceLocation("controlify", "walk_forward")));
register(WALK_BACKWARD = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_BACKWARD, new ResourceLocation("controlify", "walk_backward")));
register(WALK_LEFT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_LEFT, new ResourceLocation("controlify", "strafe_left")));
register(WALK_RIGHT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_RIGHT, new ResourceLocation("controlify", "strafe_right")));
register(LOOK_UP = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_FORWARD, new ResourceLocation("controlify", "look_up")));
register(LOOK_DOWN = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_BACKWARD, new ResourceLocation("controlify", "look_down")));
register(LOOK_LEFT = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_LEFT, new ResourceLocation("controlify", "look_left")));
register(LOOK_RIGHT = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_RIGHT, new ResourceLocation("controlify", "look_right")));
register(JUMP = new ControllerBinding<>(controller, GamepadBind.A_BUTTON, new ResourceLocation("controlify", "jump"), options.keyJump, () -> false));
register(SNEAK = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_PRESS, new ResourceLocation("controlify", "sneak"), options.keyShift, () -> controller.config().toggleSneak));
register(ATTACK = new ControllerBinding<>(controller, GamepadBind.RIGHT_TRIGGER, new ResourceLocation("controlify", "attack"), options.keyAttack, () -> false));
register(USE = new ControllerBinding<>(controller, GamepadBind.LEFT_TRIGGER, new ResourceLocation("controlify", "use"), options.keyUse, () -> false));
register(SPRINT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "sprint"), options.keySprint, () -> controller.config().toggleSprint));
register(DROP = new ControllerBinding<>(controller, GamepadBind.DPAD_DOWN, new ResourceLocation("controlify", "drop"), options.keyDrop, () -> false));
register(NEXT_SLOT = new ControllerBinding<>(controller, GamepadBind.RIGHT_BUMPER, new ResourceLocation("controlify", "next_slot")));
register(PREV_SLOT = new ControllerBinding<>(controller, GamepadBind.LEFT_BUMPER, new ResourceLocation("controlify", "prev_slot")));
register(PAUSE = new ControllerBinding<>(controller, GamepadBind.START, new ResourceLocation("controlify", "pause")));
register(INVENTORY = new ControllerBinding<>(controller, GamepadBind.Y_BUTTON, new ResourceLocation("controlify", "inventory"), options.keyInventory, () -> false));
register(CHANGE_PERSPECTIVE = new ControllerBinding<>(controller, GamepadBind.BACK, new ResourceLocation("controlify", "change_perspective"), options.keyTogglePerspective, () -> false));
register(SWAP_HANDS = new ControllerBinding<>(controller, GamepadBind.X_BUTTON, new ResourceLocation("controlify", "swap_hands"), options.keySwapOffhand, () -> false));
register(OPEN_CHAT = new ControllerBinding<>(controller, GamepadBind.DPAD_UP, new ResourceLocation("controlify", "open_chat"), options.keyChat, () -> false));
register(GUI_PRESS = new ControllerBinding<>(controller, GamepadBind.A_BUTTON, new ResourceLocation("controlify", "gui_press")));
register(GUI_BACK = new ControllerBinding<>(controller, GamepadBind.B_BUTTON, new ResourceLocation("controlify", "gui_back")));
register(GUI_NEXT_TAB = new ControllerBinding<>(controller, GamepadBind.RIGHT_BUMPER, new ResourceLocation("controlify", "gui_next_tab")));
register(GUI_PREV_TAB = new ControllerBinding<>(controller, GamepadBind.LEFT_BUMPER, new ResourceLocation("controlify", "gui_prev_tab")));
register(PICK_BLOCK = new ControllerBinding<>(controller, GamepadBind.DPAD_LEFT, new ResourceLocation("controlify", "pick_block"), options.keyPickItem, () -> false));
register(TOGGLE_HUD_VISIBILITY = new ControllerBinding<>(controller, new EmptyBind<>(), new ResourceLocation("controlify", "toggle_hud_visibility")));
register(SHOW_PLAYER_LIST = new ControllerBinding<>(controller, GamepadBind.DPAD_RIGHT, new ResourceLocation("controlify", "show_player_list"), options.keyPlayerList, () -> false));
register(VMOUSE_MOVE_UP = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_FORWARD, new ResourceLocation("controlify", "vmouse_move_up")));
register(VMOUSE_MOVE_DOWN = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_BACKWARD, new ResourceLocation("controlify", "vmouse_move_down")));
register(VMOUSE_MOVE_LEFT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_LEFT, new ResourceLocation("controlify", "vmouse_move_left")));
register(VMOUSE_MOVE_RIGHT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_RIGHT, new ResourceLocation("controlify", "vmouse_move_right")));
register(VMOUSE_LCLICK = new ControllerBinding<>(controller, GamepadBind.A_BUTTON, new ResourceLocation("controlify", "vmouse_lclick")));
register(VMOUSE_RCLICK = new ControllerBinding<>(controller, GamepadBind.X_BUTTON, new ResourceLocation("controlify", "vmouse_rclick")));
register(VMOUSE_SHIFT_CLICK = new ControllerBinding<>(controller, GamepadBind.Y_BUTTON, new ResourceLocation("controlify", "vmouse_shift_click")));
register(VMOUSE_SCROLL_UP = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_FORWARD, new ResourceLocation("controlify", "vmouse_scroll_up")));
register(VMOUSE_SCROLL_DOWN = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_BACKWARD, new ResourceLocation("controlify", "vmouse_scroll_down")));
register(VMOUSE_ESCAPE = new ControllerBinding<>(controller, GamepadBind.B_BUTTON, new ResourceLocation("controlify", "vmouse_escape")));
register(VMOUSE_SHIFT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_PRESS, new ResourceLocation("controlify", "vmouse_shift")));
register(VMOUSE_TOGGLE = new ControllerBinding<>(controller, GamepadBind.BACK, new ResourceLocation("controlify", "vmouse_toggle")));
register(GUI_NAVI_UP = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_FORWARD, new ResourceLocation("controlify", "gui_navi_up")));
register(GUI_NAVI_DOWN = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_BACKWARD, new ResourceLocation("controlify", "gui_navi_down")));
register(GUI_NAVI_LEFT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_LEFT, new ResourceLocation("controlify", "gui_navi_left")));
register(GUI_NAVI_RIGHT = new ControllerBinding<>(controller, GamepadBind.LEFT_STICK_RIGHT, new ResourceLocation("controlify", "gui_navi_right")));
register(YACL_CYCLE_OPT_FORWARD = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_RIGHT, new ResourceLocation("controlify", "yacl_cycle_opt_forward")));
register(YACL_CYCLE_OPT_BACKWARD = new ControllerBinding<>(controller, GamepadBind.RIGHT_STICK_LEFT, new ResourceLocation("controlify", "yacl_cycle_opt_backward")));
ControlifyEvents.CONTROLLER_BIND_REGISTRY.invoker().onRegisterControllerBinds(this, controller);
@ -79,16 +101,16 @@ public class ControllerBindings {
ControlifyEvents.INPUT_MODE_CHANGED.register(mode -> KeyMapping.releaseAll());
}
public BindingSupplier register(ControllerBinding binding) {
public BindingSupplier<T> register(ControllerBinding<T> binding) {
registry.put(binding.id(), binding);
return controller -> controller.bindings().get(binding.id());
}
public ControllerBinding get(ResourceLocation id) {
public ControllerBinding<T> get(ResourceLocation id) {
return registry.get(id);
}
public Map<ResourceLocation, ControllerBinding> registry() {
public Map<ResourceLocation, ControllerBinding<T>> registry() {
return Collections.unmodifiableMap(registry);
}
@ -102,13 +124,13 @@ public class ControllerBindings {
public void fromJson(JsonObject json) {
for (var binding : registry().values()) {
var bind = json.get(binding.id().toString());
var bind = json.get(binding.id().toString()).getAsJsonObject();
if (bind == null) continue;
binding.setCurrentBind(IBind.fromJson(bind));
binding.setCurrentBind(IBind.fromJson(bind, controller));
}
}
public void onControllerUpdate(Controller controller) {
public void onControllerUpdate(Controller<?, ?> controller) {
if (controller != this.controller) return;
imitateVanillaClick();

View File

@ -0,0 +1,38 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.gui.DrawSize;
public class EmptyBind<T extends ControllerState> implements IBind<T> {
public static final String BIND_ID = "empty";
@Override
public float state(T state) {
return 0;
}
@Override
public void draw(PoseStack matrices, int x, int centerY, Controller<T, ?> controller) {
}
@Override
public DrawSize drawSize() {
return new DrawSize(0, 0);
}
@Override
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("type", BIND_ID);
return object;
}
@Override
public boolean equals(Object obj) {
return obj instanceof EmptyBind;
}
}

View File

@ -0,0 +1,110 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.gamepad.GamepadConfig;
import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.gui.GuiComponent;
import net.minecraft.resources.ResourceLocation;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public enum GamepadBind implements IBind<GamepadState> {
A_BUTTON(state -> state.gamepadButtons().a(), "a_button"),
B_BUTTON(state -> state.gamepadButtons().b(), "b_button"),
X_BUTTON(state -> state.gamepadButtons().x(), "x_button"),
Y_BUTTON(state -> state.gamepadButtons().y(), "y_button"),
LEFT_BUMPER(state -> state.gamepadButtons().leftBumper(), "left_bumper"),
RIGHT_BUMPER(state -> state.gamepadButtons().rightBumper(), "right_bumper"),
LEFT_STICK_PRESS(state -> state.gamepadButtons().leftStick(), "left_stick_press"),
RIGHT_STICK_PRESS(state -> state.gamepadButtons().rightStick(), "right_stick_press"),
START(state -> state.gamepadButtons().start(), "start"),
BACK(state -> state.gamepadButtons().back(), "back"),
GUIDE(state -> state.gamepadButtons().guide(), "guide"), // the middle button
DPAD_UP(state -> state.gamepadButtons().dpadUp(), "dpad_up"),
DPAD_DOWN(state -> state.gamepadButtons().dpadDown(), "dpad_down"),
DPAD_LEFT(state -> state.gamepadButtons().dpadLeft(), "dpad_left"),
DPAD_RIGHT(state -> state.gamepadButtons().dpadRight(), "dpad_right"),
LEFT_TRIGGER(state -> state.gamepadAxes().leftTrigger(), "left_trigger", true),
RIGHT_TRIGGER(state -> state.gamepadAxes().rightTrigger(), "right_trigger", true),
LEFT_STICK_FORWARD(state -> -Math.min(0, state.gamepadAxes().leftStickY()), "left_stick_up", true),
LEFT_STICK_BACKWARD(state -> Math.max(0, state.gamepadAxes().leftStickY()), "left_stick_down", true),
LEFT_STICK_LEFT(state -> -Math.min(0, state.gamepadAxes().leftStickX()), "left_stick_left", true),
LEFT_STICK_RIGHT(state -> Math.max(0, state.gamepadAxes().leftStickX()), "left_stick_right", true),
RIGHT_STICK_FORWARD(state -> -Math.min(0, state.gamepadAxes().rightStickY()), "right_stick_up", true),
RIGHT_STICK_BACKWARD(state -> Math.max(0, state.gamepadAxes().rightStickY()), "right_stick_down", true),
RIGHT_STICK_LEFT(state -> -Math.min(0, state.gamepadAxes().rightStickX()), "right_stick_left", true),
RIGHT_STICK_RIGHT(state -> Math.max(0, state.gamepadAxes().rightStickX()), "right_stick_right", true);
public static final String BIND_ID = "gamepad";
private final Function<GamepadState, Float> state;
private final String identifier;
private final Map<BuiltinGamepadTheme, ResourceLocation> textureLocations;
GamepadBind(Function<GamepadState, Float> state, String identifier, boolean jvmIsBad) {
this.state = state;
this.identifier = identifier;
this.textureLocations = new HashMap<>();
for (BuiltinGamepadTheme theme : BuiltinGamepadTheme.values()) {
if (theme == BuiltinGamepadTheme.DEFAULT) continue;
textureLocations.put(theme, new ResourceLocation("controlify", "textures/gui/gamepad_buttons/" + theme.id() + "/" + identifier + ".png"));
}
}
GamepadBind(Function<GamepadState, Boolean> state, String identifier) {
this(state1 -> state.apply(state1) ? 1f : 0f, identifier, true);
}
@Override
public float state(GamepadState state) {
return this.state.apply(state);
}
@Override
public void draw(PoseStack matrices, int x, int centerY, Controller<GamepadState, ?> controller) {
ResourceLocation texture;
if (((GamepadConfig)controller.config()).theme == BuiltinGamepadTheme.DEFAULT) {
texture = new ResourceLocation("controlify", "textures/gui/gamepad_buttons/" + controller.type().identifier() + "/" + identifier + ".png");
} else {
texture = textureLocations.get(((GamepadConfig)controller.config()).theme);
}
RenderSystem.setShaderTexture(0, texture);
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
GuiComponent.blit(matrices, x, centerY - 22 / 2, 0, 0, 22, 22, 22, 22);
}
@Override
public DrawSize drawSize() {
return new DrawSize(22, 22);
}
public String identifier() {
return identifier;
}
@Override
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("type", BIND_ID);
object.addProperty("bind", identifier);
return object;
}
public static GamepadBind fromJson(JsonObject object) {
String name = object.get("bind").getAsString();
for (GamepadBind bind : values()) {
if (bind.identifier.equals(name)) return bind;
}
return null;
}
}

View File

@ -1,38 +1,45 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
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 dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.*;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.gui.DrawSize;
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;
public interface IBind<S extends ControllerState> {
float state(S state);
default boolean held(S state, Controller<S, ?> controller) {
return state(state) > controller.config().buttonActivationThreshold;
}
void draw(PoseStack matrices, int x, int centerY, Controller controller);
ButtonRenderer.DrawSize drawSize();
void draw(PoseStack matrices, int x, int centerY, Controller<S, ?> controller);
DrawSize drawSize();
JsonElement toJson();
JsonObject 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());
@SuppressWarnings("unchecked")
static <T extends ControllerState> IBind<T> fromJson(JsonObject json, Controller<T, ?> controller) {
var type = json.get("type").getAsString();
if (type.equals(EmptyBind.BIND_ID))
return new EmptyBind<>();
if (controller instanceof GamepadController && type.equals(GamepadBind.BIND_ID)) {
return (IBind<T>) GamepadBind.fromJson(json);
} else if (controller instanceof JoystickController joystick) {
return (IBind<T>) switch (type) {
case JoystickButtonBind.BIND_ID -> JoystickButtonBind.fromJson(json, joystick);
case JoystickHatBind.BIND_ID -> JoystickHatBind.fromJson(json, joystick);
case JoystickAxisBind.BIND_ID -> JoystickAxisBind.fromJson(json, joystick);
default -> {
Controlify.LOGGER.error("Unknown bind type: " + type);
yield new EmptyBind<>();
}
};
}
}
static IBind create(Collection<Bind> 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);
Controlify.LOGGER.error("Could not parse bind for controller: " + controller.name());
return new EmptyBind<>();
}
}

View File

@ -0,0 +1,88 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import java.util.Objects;
public class JoystickAxisBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_axis";
private final JoystickController joystick;
private final int axisIndex;
private final AxisDirection direction;
public JoystickAxisBind(JoystickController joystick, int axisIndex, AxisDirection direction) {
this.joystick = joystick;
this.axisIndex = axisIndex;
this.direction = direction;
}
@Override
public float state(JoystickState state) {
var rawState = state.axes().get(axisIndex);
return switch (direction) {
case POSITIVE -> Math.max(0, rawState);
case NEGATIVE -> -Math.min(0, rawState);
};
}
@Override
public void draw(PoseStack matrices, int x, int centerY, Controller<JoystickState, ?> controller) {
var font = Minecraft.getInstance().font;
font.drawShadow(matrices, getTempButtonName(), x + 1.5f, centerY - font.lineHeight / 2f, 0xFFFFFF);
}
@Override
public DrawSize drawSize() {
var font = Minecraft.getInstance().font;
return new DrawSize(font.width(getTempButtonName()) + 3, font.lineHeight);
}
private Component getTempButtonName() {
var axis = joystick.mapping().axis(axisIndex);
return Component.empty()
.append(axis.name())
.append(" ")
.append(axis.getDirectionName(axisIndex, direction));
}
@Override
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("type", BIND_ID);
object.addProperty("axis", axisIndex);
object.addProperty("direction", direction.name());
return object;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JoystickAxisBind that = (JoystickAxisBind) o;
return axisIndex == that.axisIndex && direction == that.direction;
}
@Override
public int hashCode() {
return Objects.hash(axisIndex, direction);
}
public static JoystickAxisBind fromJson(JsonObject object, JoystickController joystick) {
var axisIndex = object.get("axis").getAsInt();
var direction = AxisDirection.valueOf(object.get("direction").getAsString());
return new JoystickAxisBind(joystick, axisIndex, direction);
}
public enum AxisDirection {
POSITIVE,
NEGATIVE
}
}

View File

@ -0,0 +1,72 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import java.util.Objects;
public class JoystickButtonBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_button";
private final JoystickController joystick;
private final int buttonIndex;
public JoystickButtonBind(JoystickController joystick, int buttonIndex) {
this.joystick = joystick;
this.buttonIndex = buttonIndex;
}
@Override
public float state(JoystickState state) {
return state.buttons().get(buttonIndex) ? 1 : 0;
}
@Override
public void draw(PoseStack matrices, int x, int centerY, Controller<JoystickState, ?> controller) {
var font = Minecraft.getInstance().font;
font.drawShadow(matrices, getTempButtonName(), x + 1.5f, centerY - font.lineHeight / 2f, 0xFFFFFF);
}
@Override
public DrawSize drawSize() {
var font = Minecraft.getInstance().font;
return new DrawSize(font.width(getTempButtonName()) + 3, font.lineHeight);
}
private Component getTempButtonName() {
return joystick.mapping().button(buttonIndex).name();
}
@Override
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("type", BIND_ID);
object.addProperty("button", buttonIndex);
return object;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JoystickButtonBind that = (JoystickButtonBind) o;
return buttonIndex == that.buttonIndex && joystick.uid().equals(that.joystick.uid());
}
@Override
public int hashCode() {
return Objects.hash(buttonIndex, joystick.uid());
}
public static JoystickButtonBind fromJson(JsonObject object, JoystickController joystick) {
var buttonIndex = object.get("button").getAsInt();
return new JoystickButtonBind(joystick, buttonIndex);
}
}

View File

@ -0,0 +1,78 @@
package dev.isxander.controlify.bindings;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import java.util.Objects;
public class JoystickHatBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_hat";
private final JoystickController joystick;
private final int hatIndex;
private final JoystickState.HatState hatState;
public JoystickHatBind(JoystickController joystick, int hatIndex, JoystickState.HatState hatState) {
this.joystick = joystick;
this.hatIndex = hatIndex;
this.hatState = hatState;
}
@Override
public float state(JoystickState state) {
return state.hats().get(hatIndex) == hatState ? 1 : 0;
}
@Override
public void draw(PoseStack matrices, int x, int centerY, Controller<JoystickState, ?> controller) {
var font = Minecraft.getInstance().font;
font.drawShadow(matrices, getTempButtonName(), x + 1.5f, centerY - font.lineHeight / 2f, 0xFFFFFF);
}
@Override
public DrawSize drawSize() {
var font = Minecraft.getInstance().font;
return new DrawSize(font.width(getTempButtonName()) + 3, font.lineHeight);
}
private Component getTempButtonName() {
return Component.empty()
.append(joystick.mapping().hat(hatIndex).name())
.append(" ")
.append(hatState.getDisplayName());
}
@Override
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("type", BIND_ID);
object.addProperty("hat", hatIndex);
object.addProperty("state", hatState.name());
return object;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JoystickHatBind that = (JoystickHatBind) o;
return hatIndex == that.hatIndex && hatState == that.hatState && joystick.uid().equals(that.joystick.uid());
}
@Override
public int hashCode() {
return Objects.hash(hatIndex, hatState, joystick.uid());
}
public static JoystickHatBind fromJson(JsonObject object, JoystickController joystick) {
var hatIndex = object.get("hat").getAsInt();
var hatState = JoystickState.HatState.valueOf(object.get("state").getAsString());
return new JoystickHatBind(joystick, hatIndex, hatState);
}
}

View File

@ -69,7 +69,7 @@ public class ControlifyConfig {
return config;
}
private JsonObject generateControllerConfig(Controller controller) {
private JsonObject generateControllerConfig(Controller<?, ?> controller) {
JsonObject object = new JsonObject();
object.add("config", GSON.toJsonTree(controller.config()));
@ -91,19 +91,21 @@ public class ControlifyConfig {
}
}
public boolean loadOrCreateControllerData(Controller controller) {
public boolean loadOrCreateControllerData(Controller<?, ?> controller) {
var uid = controller.uid();
if (controllerData.has(uid)) {
Controlify.LOGGER.info("Loading controller data for " + uid);
applyControllerConfig(controller, controllerData.getAsJsonObject(uid));
return true;
} else {
Controlify.LOGGER.info("New controller found, creating controller data for " + uid);
save();
return false;
}
}
private void applyControllerConfig(Controller controller, JsonObject object) {
controller.setConfig(GSON.fromJson(object.getAsJsonObject("config"), Controller.ControllerConfig.class));
private void applyControllerConfig(Controller<?, ?> controller, JsonObject object) {
controller.setConfig(GSON, object.getAsJsonObject("config"));
controller.bindings().fromJson(object.getAsJsonObject("bindings"));
}

View File

@ -1,8 +1,10 @@
package dev.isxander.controlify.config.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.Bind;
import dev.isxander.controlify.bindings.GamepadBind;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
@ -16,20 +18,17 @@ import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.LinkedHashSet;
import java.util.Set;
public class GamepadBindController implements Controller<IBind<GamepadState>> {
private final Option<IBind<GamepadState>> option;
private final GamepadController controller;
public class BindButtonController implements Controller<IBind> {
private final Option<IBind> option;
private final dev.isxander.controlify.controller.Controller controller;
public BindButtonController(Option<IBind> option, dev.isxander.controlify.controller.Controller controller) {
public GamepadBindController(Option<IBind<GamepadState>> option, GamepadController controller) {
this.option = option;
this.controller = controller;
}
@Override
public Option<IBind> option() {
public Option<IBind<GamepadState>> option() {
return this.option;
}
@ -43,26 +42,18 @@ public class BindButtonController implements Controller<IBind> {
return new BindButtonWidget(this, yaclScreen, dimension);
}
public static class BindButtonWidget extends ControllerWidget<BindButtonController> implements ComponentProcessorProvider, ComponentProcessor {
public static class BindButtonWidget extends ControllerWidget<GamepadBindController> implements ComponentProcessorProvider, ComponentProcessor {
private boolean awaitingControllerInput = false;
private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC);
private final Set<Bind> pressedBinds = new LinkedHashSet<>();
public BindButtonWidget(BindButtonController control, YACLScreen screen, Dimension<Integer> dim) {
public BindButtonWidget(GamepadBindController control, YACLScreen screen, Dimension<Integer> dim) {
super(control, screen, dim);
}
@Override
protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) {
if (awaitingControllerInput) {
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);
}
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
} else {
var bind = control.option().pendingValue();
bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width(), getDimension().centerY(), control.controller);
@ -95,41 +86,31 @@ public class BindButtonController implements Controller<IBind> {
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
if (controller != control.controller) return true;
if (controller.bindings().GUI_PRESS.justPressed() && !awaitingControllerInput) {
return awaitingControllerInput = true;
}
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)) {
if (bind == Bind.GUIDE) { // FIXME: guide cannot be used as reserve because Windows hooks into xbox button to open game bar, maybe START?
if (pressedBinds.isEmpty()) {
awaitingControllerInput = false;
control.option().requestSet(IBind.create(Bind.NONE));
pressedBinds.clear();
return true;
}
} else {
pressedBinds.add(bind);
}
}
var gamepad = control.controller;
for (var bind : GamepadBind.values()) {
if (bind.held(gamepad.state(), gamepad) && !bind.held(gamepad.prevState(), gamepad)) {
control.option().requestSet(bind);
awaitingControllerInput = false;
gamepad.consumeButtonState();
return true;
}
control.controller.consumeButtonState();
}
return true;
return false;
}
@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;
}

View File

@ -0,0 +1,164 @@
package dev.isxander.controlify.config.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.*;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.yacl.api.Controller;
import dev.isxander.yacl.api.Option;
import dev.isxander.yacl.api.utils.Dimension;
import dev.isxander.yacl.gui.AbstractWidget;
import dev.isxander.yacl.gui.YACLScreen;
import dev.isxander.yacl.gui.controllers.ControllerWidget;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
public class JoystickBindController implements Controller<IBind<JoystickState>> {
private final Option<IBind<JoystickState>> option;
private final JoystickController controller;
public JoystickBindController(Option<IBind<JoystickState>> option, JoystickController controller) {
this.option = option;
this.controller = controller;
}
@Override
public Option<IBind<JoystickState>> option() {
return this.option;
}
@Override
public Component formatValue() {
return Component.empty();
}
@Override
public AbstractWidget provideWidget(YACLScreen yaclScreen, Dimension<Integer> dimension) {
return new BindButtonWidget(this, yaclScreen, dimension);
}
public static class BindButtonWidget extends ControllerWidget<JoystickBindController> implements ComponentProcessorProvider, ComponentProcessor {
private boolean awaitingControllerInput = false;
private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC);
public BindButtonWidget(JoystickBindController control, YACLScreen screen, Dimension<Integer> dim) {
super(control, screen, dim);
}
@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);
} else {
var bind = control.option().pendingValue();
bind.draw(matrices, getDimension().xLimit() - bind.drawSize().width(), getDimension().centerY(), control.controller);
}
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (isFocused() && keyCode == GLFW.GLFW_KEY_ENTER && !awaitingControllerInput) {
awaitingControllerInput = true;
return true;
}
return false;
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (getDimension().isPointInside((int)mouseX, (int)mouseY)) {
awaitingControllerInput = true;
return true;
}
return false;
}
@Override
public ComponentProcessor componentProcessor() {
return this;
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
if (controller != control.controller) return true;
if (controller.bindings().GUI_PRESS.justPressed() && !awaitingControllerInput) {
return awaitingControllerInput = true;
}
if (!awaitingControllerInput) return false;
var joystick = control.controller;
var state = joystick.state();
var prevState = joystick.prevState();
for (int i = 0; i < Math.min(state.buttons().size(), prevState.buttons().size()); i++) {
if (state.buttons().get(i) && !prevState.buttons().get(i)) {
control.option().requestSet(new JoystickButtonBind(joystick, i));
awaitingControllerInput = false;
return true;
}
}
for (int i = 0; i < Math.min(state.axes().size(), prevState.axes().size()); i++) {
var axis = state.axes().get(i);
var prevAxis = prevState.axes().get(i);
var activationThreshold = joystick.config().buttonActivationThreshold;
if (Math.abs(prevAxis) < activationThreshold) {
if (axis > activationThreshold) {
control.option().requestSet(new JoystickAxisBind(joystick, i, JoystickAxisBind.AxisDirection.POSITIVE));
awaitingControllerInput = false;
return true;
} else if (axis < -activationThreshold) {
control.option().requestSet(new JoystickAxisBind(joystick, i, JoystickAxisBind.AxisDirection.NEGATIVE));
awaitingControllerInput = false;
return true;
}
}
}
for (int i = 0; i < Math.min(state.hats().size(), prevState.hats().size()); i++) {
var hat = state.hats().get(i);
var prevHat = prevState.hats().get(i);
if (prevHat.isCentered() && !hat.isCentered()) {
control.option().requestSet(new JoystickHatBind(joystick, i, hat));
awaitingControllerInput = false;
return true;
}
}
return false;
}
@Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, dev.isxander.controlify.controller.Controller<?, ?> controller) {
return awaitingControllerInput;
}
@Override
protected int getHoveredControlWidth() {
return getUnhoveredControlWidth();
}
@Override
protected int getUnhoveredControlWidth() {
if (awaitingControllerInput)
return textRenderer.width(awaitingText);
return control.option().pendingValue().drawSize().width();
}
}
}

View File

@ -3,8 +3,12 @@ package dev.isxander.controlify.config.gui;
import dev.isxander.controlify.Controlify;
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.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.yacl.api.*;
import dev.isxander.yacl.gui.controllers.ActionController;
@ -22,6 +26,10 @@ import net.minecraft.client.gui.screens.AlertScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class YACLHelper {
public static Screen generateConfigScreen(Screen parent) {
if (Controlify.instance().currentController() == null) {
@ -41,11 +49,11 @@ public class YACLHelper {
var globalSettings = Controlify.instance().config().globalSettings();
var globalCategory = ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.category.global"))
.option(Option.createBuilder(Controller.class)
.option(Option.createBuilder((Class<Controller<?, ?>>) (Class<?>) Controller.class)
.name(Component.translatable("controlify.gui.current_controller"))
.tooltip(Component.translatable("controlify.gui.current_controller.tooltip"))
.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())))
.controller(opt -> new CyclingListController<>(opt, Controller.CONTROLLERS.values(), c -> Component.literal(c.name())))
.instant(true)
.build())
.option(Option.createBuilder(boolean.class)
@ -114,14 +122,22 @@ public class YACLHelper {
.tooltip(Component.translatable("controlify.gui.vmouse_sensitivity.tooltip"))
.binding(def.virtualMouseSensitivity, () -> config.virtualMouseSensitivity, v -> config.virtualMouseSensitivity = v)
.controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build())
.option(Option.createBuilder(ControllerTheme.class)
.name(Component.translatable("controlify.gui.controller_theme"))
.tooltip(Component.translatable("controlify.gui.controller_theme.tooltip"))
.binding(controller.type().theme(), () -> config.theme, v -> config.theme = v)
.controller(EnumController::new)
.instant(true)
.build())
.build());
if (controller instanceof GamepadController gamepad) {
var gamepadConfig = gamepad.config();
var defaultGamepadConfig = gamepad.defaultConfig();
basicGroup.option(Option.createBuilder(BuiltinGamepadTheme.class)
.name(Component.translatable("controlify.gui.controller_theme"))
.tooltip(Component.translatable("controlify.gui.controller_theme.tooltip"))
.binding(defaultGamepadConfig.theme, () -> gamepadConfig.theme, v -> gamepadConfig.theme = v)
.controller(EnumController::new)
.instant(true)
.build());
}
basicGroup
.option(Option.createBuilder(String.class)
.name(Component.translatable("controlify.gui.custom_name"))
.tooltip(Component.translatable("controlify.gui.custom_name.tooltip"))
@ -139,21 +155,59 @@ public class YACLHelper {
.tooltip(Component.translatable("controlify.gui.screen_repeat_navi_delay.tooltip"))
.binding(def.screenRepeatNavigationDelay, () -> config.screenRepeatNavigationDelay, v -> config.screenRepeatNavigationDelay = v)
.controller(opt -> new IntegerSliderController(opt, 1, 20, 1, v -> Component.translatable("controlify.gui.format.ticks", v)))
.build())
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.left_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip"))
.build());
if (controller instanceof GamepadController gamepad) {
var gpCfg = gamepad.config();
var gpCfgDef = gamepad.defaultConfig();
advancedGroup
.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.left_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
)
.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.right_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
)
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build());
} else if (controller instanceof JoystickController joystick) {
Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount())
.filter(i -> joystick.mapping().axis(i).requiresDeadzone())
.boxed()
.collect(Collectors.toMap(
i -> joystick.mapping().axis(i).identifier(),
i -> i,
(x, y) -> x
))
.values();
var jsCfg = joystick.config();
var jsCfgDef = joystick.defaultConfig();
for (int i : deadzoneAxes) {
advancedGroup.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", joystick.mapping().axis(i).name()))
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", joystick.mapping().axis(i).name()))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(def.leftStickDeadzone, () -> config.leftStickDeadzone, v -> config.leftStickDeadzone = v)
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
.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.right_stick_deadzone"))
.tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip"))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(def.rightStickDeadzone, () -> config.rightStickDeadzone, v -> config.rightStickDeadzone = v)
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build())
.build());
}
}
advancedGroup
.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.auto_calibration"))
.tooltip(Component.translatable("controlify.gui.auto_calibration.tooltip"))
@ -170,19 +224,26 @@ 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(IBind.class)
.name(control.name())
.binding(control.defaultBind(), control::currentBind, control::setCurrentBind)
.controller(opt -> new BindButtonController(opt, controller))
.tooltip(control.description())
.instant(true)
.listener((opt, bind) -> { // yacl instant options have a bug where they don't save
opt.applyValue();
controlify.config().save();
})
.build());
if (controller instanceof GamepadController gamepad) {
for (var binding : gamepad.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder((Class<IBind<GamepadState>>) (Class<?>) IBind.class)
.name(binding.name())
.binding(binding.defaultBind(), binding::currentBind, binding::setCurrentBind)
.controller(opt -> new GamepadBindController(opt, gamepad))
.tooltip(binding.description())
.build());
}
} else if (controller instanceof JoystickController joystick) {
for (var binding : joystick.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder((Class<IBind<JoystickState>>) (Class<?>) IBind.class)
.name(binding.name())
.binding(binding.defaultBind(), binding::currentBind, binding::setCurrentBind)
.controller(opt -> new JoystickBindController(opt, joystick))
.tooltip(binding.description())
.build());
}
}
category.group(controlsGroup.build());
yacl.category(category.build());

View File

@ -0,0 +1,123 @@
package dev.isxander.controlify.controller;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.hid.HIDIdentifier;
import org.hid4java.HidDevice;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractController<S extends ControllerState, C extends ControllerConfig> implements Controller<S, C> {
private final int joystickId;
protected String name;
private final String uid;
private final String guid;
private final ControllerType type;
private final ControllerBindings<S> bindings;
protected C config, defaultConfig;
public AbstractController(int joystickId, @Nullable HidDevice hidDevice) {
if (joystickId > GLFW.GLFW_JOYSTICK_LAST || joystickId < 0)
throw new IllegalArgumentException("Joystick ID " + joystickId + " is out of range!");
if (!GLFW.glfwJoystickPresent(joystickId))
throw new IllegalArgumentException("Joystick " + joystickId + " is not present and cannot be initialised!");
this.joystickId = joystickId;
this.guid = GLFW.glfwGetJoystickGUID(joystickId);
if (hidDevice != null) {
this.uid = UUID.nameUUIDFromBytes(hidDevice.getPath().getBytes()).toString();
this.type = ControllerType.getTypeForHID(new HIDIdentifier(hidDevice.getVendorId(), hidDevice.getProductId()));
} else {
this.uid = "unidentified-guid-" + UUID.nameUUIDFromBytes(this.guid.getBytes());
this.type = ControllerType.UNKNOWN;
}
var joystickName = GLFW.glfwGetJoystickName(joystickId);
String name = type != ControllerType.UNKNOWN || joystickName == null ? type.friendlyName() : joystickName;
setName(name);
this.bindings = new ControllerBindings<>(this);
}
@Override
public int joystickId() {
return this.joystickId;
}
public String name() {
if (config().customName != null)
return config().customName;
return name;
}
protected void setName(String name) {
String uniqueName = name;
int i = 0;
while (Controller.CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equalsIgnoreCase)) {
uniqueName = name + " (" + i++ + ")";
}
this.name = uniqueName;
}
@Override
public String uid() {
return this.uid;
}
@Override
public String guid() {
return this.guid;
}
@Override
public ControllerType type() {
return this.type;
}
@Override
public ControllerBindings<S> bindings() {
return this.bindings;
}
@Override
public C config() {
return this.config;
}
@Override
public C defaultConfig() {
return this.defaultConfig;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
C newConfig = gson.fromJson(json, new TypeToken<C>(getClass()){}.getType());
if (newConfig != null) {
this.config = newConfig;
} else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AbstractController<?, ?> that = (AbstractController<?, ?>) o;
return uid.equals(that.uid);
}
@Override
public int hashCode() {
return Objects.hash(uid);
}
}

View File

@ -1,65 +0,0 @@
package dev.isxander.controlify.controller;
import org.lwjgl.glfw.GLFW;
public record AxesState(
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger
) {
public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0);
public AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) {
return new AxesState(
deadzone(leftStickX, deadZoneX),
deadzone(leftStickY, deadZoneY),
rightStickX, rightStickY, leftTrigger, rightTrigger
);
}
public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) {
return new AxesState(
leftStickX, leftStickY,
deadzone(rightStickX, deadZoneX),
deadzone(rightStickY, deadZoneY),
leftTrigger, rightTrigger
);
}
public AxesState leftTriggerDeadZone(float deadZone) {
return new AxesState(
leftStickX, leftStickY, rightStickX, rightStickY,
deadzone(leftTrigger, deadZone),
rightTrigger
);
}
public AxesState rightTriggerDeadZone(float deadZone) {
return new AxesState(
leftStickX, leftStickY, rightStickX, rightStickY,
leftTrigger,
deadzone(rightTrigger, deadZone)
);
}
private float deadzone(float value, float deadzone) {
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
}
public static AxesState fromController(Controller controller) {
if (controller == null || !controller.connected())
return EMPTY;
var state = controller.getGamepadState();
var axes = state.axes();
float leftX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X);
float leftY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y);
float rightX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X);
float rightY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y);
float leftTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER) + 1f) / 2f;
float rightTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) + 1f) / 2f;
return new AxesState(leftX, leftY, rightX, rightY, leftTrigger, rightTrigger);
}
}

View File

@ -1,45 +0,0 @@
package dev.isxander.controlify.controller;
import org.lwjgl.glfw.GLFW;
public record ButtonState(
boolean a, boolean b, boolean x, boolean y,
boolean leftBumper, boolean rightBumper,
boolean back, boolean start, boolean guide,
boolean dpadUp, boolean dpadDown, boolean dpadLeft, boolean dpadRight,
boolean leftStick, boolean rightStick
) {
public static ButtonState EMPTY = new ButtonState(
false, false, false, false,
false, false,
false, false, false,
false, false, false, false,
false, false
);
public static ButtonState fromController(Controller controller) {
if (controller == null || !controller.connected())
return EMPTY;
var state = controller.getGamepadState();
var buttons = state.buttons();
boolean a = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_A) == GLFW.GLFW_PRESS;
boolean b = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_B) == GLFW.GLFW_PRESS;
boolean x = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_X) == GLFW.GLFW_PRESS;
boolean y = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_Y) == GLFW.GLFW_PRESS;
boolean leftBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == GLFW.GLFW_PRESS;
boolean rightBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == GLFW.GLFW_PRESS;
boolean back = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_BACK) == GLFW.GLFW_PRESS;
boolean start = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_START) == GLFW.GLFW_PRESS;
boolean guide = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_GUIDE) == GLFW.GLFW_PRESS;
boolean dpadUp = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW.GLFW_PRESS;
boolean dpadDown = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW.GLFW_PRESS;
boolean dpadLeft = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW.GLFW_PRESS;
boolean dpadRight = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW.GLFW_PRESS;
boolean leftStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_THUMB) == GLFW.GLFW_PRESS;
boolean rightStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB) == GLFW.GLFW_PRESS;
return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick);
}
}

View File

@ -1,193 +1,127 @@
package dev.isxander.controlify.controller;
import dev.isxander.controlify.Controlify;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.hid.HIDIdentifier;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.joystick.JoystickController;
import org.hid4java.HidDevice;
import org.jetbrains.annotations.Nullable;
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<Integer, Controller> CONTROLLERS = new HashMap<>();
public static final Controller DUMMY = new Controller(-1, "DUMMY", "DUMMY", false, UUID.randomUUID().toString(), ControllerType.UNKNOWN);
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
String uid();
int joystickId();
String guid();
private final int joystickId;
private final String guid;
private final String name;
private final boolean gamepad;
private final String uid;
private final ControllerType type;
ControllerBindings<S> bindings();
private ControllerState state = ControllerState.EMPTY;
private ControllerState prevState = ControllerState.EMPTY;
S state();
S prevState();
private final ControllerBindings bindings = new ControllerBindings(this);
private ControllerConfig config, defaultConfig;
C config();
C defaultConfig();
void setConfig(Gson gson, JsonElement json);
public Controller(int joystickId, String guid, String name, boolean gamepad, String uid, ControllerType type) {
this.joystickId = joystickId;
this.guid = guid;
this.name = name;
this.gamepad = gamepad;
this.uid = uid;
this.type = type;
this.config = new ControllerConfig();
this.defaultConfig = new ControllerConfig();
}
ControllerType type();
public ControllerState state() {
return state;
}
String name();
public ControllerState prevState() {
return prevState;
}
void updateState();
public void updateState() {
if (!connected()) {
state = prevState = ControllerState.EMPTY;
return;
Map<Integer, Controller<?, ?>> CONTROLLERS = new HashMap<>();
static Controller<?, ?> createOrGet(int joystickId, @Nullable HidDevice device) {
if (CONTROLLERS.containsKey(joystickId)) {
return CONTROLLERS.get(joystickId);
}
prevState = state;
AxesState rawAxesState = AxesState.fromController(this);
AxesState axesState = rawAxesState
.leftJoystickDeadZone(config().leftStickDeadzone, config().leftStickDeadzone)
.rightJoystickDeadZone(config().rightStickDeadzone, config().rightStickDeadzone)
.leftTriggerDeadZone(config().leftTriggerDeadzone)
.rightTriggerDeadZone(config().rightTriggerDeadzone);
ButtonState buttonState = ButtonState.fromController(this);
state = new ControllerState(axesState, rawAxesState, buttonState);
}
public void consumeButtonState() {
this.state = new ControllerState(state().axes(), state().rawAxes(), ButtonState.EMPTY);
}
public ControllerBindings bindings() {
return bindings;
}
public boolean connected() {
return GLFW.glfwJoystickPresent(joystickId);
}
GLFWGamepadState getGamepadState() {
GLFWGamepadState state = GLFWGamepadState.create();
if (gamepad)
GLFW.glfwGetGamepadState(joystickId, state);
return state;
}
public int id() {
return joystickId;
}
public String guid() {
return guid;
}
public String uid() {
return uid;
}
public ControllerType type() {
return type;
}
public String name() {
if (config().customName != null)
return config().customName;
return name;
}
public boolean gamepad() {
return gamepad;
}
public ControllerConfig config() {
return config;
}
public ControllerConfig defaultConfig() {
return defaultConfig;
}
public void setConfig(ControllerConfig config) {
this.config = config;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Controller) obj;
return Objects.equals(this.guid, that.guid);
}
@Override
public int hashCode() {
return Objects.hash(guid);
}
public static Controller create(int id, @Nullable HidDevice device) {
if (id > GLFW.GLFW_JOYSTICK_LAST)
throw new IllegalArgumentException("Invalid joystick id: " + id);
if (CONTROLLERS.containsKey(id))
return CONTROLLERS.get(id);
String guid = GLFW.glfwGetJoystickGUID(id);
boolean gamepad = GLFW.glfwJoystickIsGamepad(id);
String fallbackName = gamepad ? GLFW.glfwGetGamepadName(id) : GLFW.glfwGetJoystickName(id);
String uid = device != null ? UUID.nameUUIDFromBytes(device.getPath().getBytes(StandardCharsets.UTF_8)).toString() : "unidentified-" + UUID.randomUUID();
ControllerType type = device != null ? ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId())) : ControllerType.UNKNOWN;
String ogName = type != ControllerType.UNKNOWN || fallbackName == null ? type.friendlyName() : fallbackName;
String name = ogName;
int tries = 1;
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equals)) {
name = ogName + " (" + tries++ + ")";
if (GLFW.glfwJoystickIsGamepad(joystickId)) {
GamepadController controller = new GamepadController(joystickId, device);
CONTROLLERS.put(joystickId, controller);
return controller;
}
Controller controller = new Controller(id, guid, name, gamepad, uid, type);
CONTROLLERS.put(id, controller);
JoystickController controller = new JoystickController(joystickId, device);
CONTROLLERS.put(joystickId, controller);
return controller;
}
public class ControllerConfig {
public float horizontalLookSensitivity = 1f;
public float verticalLookSensitivity = 0.9f;
Controller<?, ?> DUMMY = new Controller<>() {
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
private final ControllerConfig config = new ControllerConfig() {
@Override
public void setDeadzone(int axis, float deadzone) {
public float leftStickDeadzone = 0.2f;
public float rightStickDeadzone = 0.2f;
}
// not sure if triggers need deadzones
public float leftTriggerDeadzone = 0.0f;
public float rightTriggerDeadzone = 0.0f;
@Override
public float getDeadzone(int axis) {
return 0;
}
};
public float buttonActivationThreshold = 0.5f;
@Override
public String uid() {
return "DUMMY";
}
public int screenRepeatNavigationDelay = 4;
@Override
public int joystickId() {
return -1;
}
public float virtualMouseSensitivity = 1f;
@Override
public String guid() {
return "DUMMY";
}
public ControllerTheme theme = type().theme();
@Override
public ControllerBindings<ControllerState> bindings() {
return bindings;
}
public boolean autoJump = false;
public boolean toggleSprint = true;
public boolean toggleSneak = true;
@Override
public ControllerConfig config() {
return config;
}
public String customName = null;
@Override
public ControllerConfig defaultConfig() {
return config;
}
public boolean showGuide = true;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
}
@Override
public ControllerType type() {
return ControllerType.UNKNOWN;
}
@Override
public String name() {
return "DUMMY";
}
@Override
public ControllerState state() {
return ControllerState.EMPTY;
}
@Override
public ControllerState prevState() {
return ControllerState.EMPTY;
}
@Override
public void updateState() {
}
};
}

View File

@ -0,0 +1,23 @@
package dev.isxander.controlify.controller;
public abstract class ControllerConfig {
public float horizontalLookSensitivity = 1f;
public float verticalLookSensitivity = 0.9f;
public float buttonActivationThreshold = 0.5f;
public int screenRepeatNavigationDelay = 4;
public float virtualMouseSensitivity = 1f;
public boolean autoJump = false;
public boolean toggleSprint = true;
public boolean toggleSneak = true;
public String customName = null;
public boolean showGuide = true;
public abstract void setDeadzone(int axis, float deadzone);
public abstract float getDeadzone(int axis);
}

View File

@ -1,9 +1,35 @@
package dev.isxander.controlify.controller;
public record ControllerState(AxesState axes, AxesState rawAxes, ButtonState buttons) {
public static final ControllerState EMPTY = new ControllerState(AxesState.EMPTY, AxesState.EMPTY, ButtonState.EMPTY);
import java.util.List;
import java.util.Set;
public boolean hasAnyInput() {
return !this.axes().equals(AxesState.EMPTY) || !this.buttons().equals(ButtonState.EMPTY);
}
public interface ControllerState {
List<Float> axes();
List<Float> rawAxes();
List<Boolean> buttons();
boolean hasAnyInput();
ControllerState EMPTY = new ControllerState() {
@Override
public List<Float> axes() {
return List.of();
}
@Override
public List<Float> rawAxes() {
return List.of();
}
@Override
public List<Boolean> buttons() {
return List.of();
}
@Override
public boolean hasAnyInput() {
return false;
}
};
}

View File

@ -2,37 +2,42 @@ package dev.isxander.controlify.controller;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import dev.isxander.controlify.controller.hid.HIDIdentifier;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.IoSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public enum ControllerType {
UNKNOWN("Unknown Controller", ControllerTheme.XBOX_ONE),
XBOX_ONE("Xbox Controller", ControllerTheme.XBOX_ONE),
XBOX_360("Xbox 360 Controller", ControllerTheme.XBOX_ONE),
DUALSHOCK4("PS4 Controller", ControllerTheme.DUALSHOCK4),
STEAM_DECK("Steam Deck", ControllerTheme.XBOX_ONE);
public class ControllerType {
public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown");
private static final Gson GSON = new GsonBuilder().setLenient().create();
private static Map<HIDIdentifier, ControllerType> typeMap = null;
private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "hiddb.json5");
private final String friendlyName;
private final ControllerTheme theme;
private final String identifier;
ControllerType(String friendlyName, ControllerTheme theme) {
private ControllerType(String friendlyName, String identifier) {
this.friendlyName = friendlyName;
this.theme = theme;
this.identifier = identifier;
}
public String friendlyName() {
return friendlyName;
}
public ControllerTheme theme() {
return theme;
public String identifier() {
return identifier;
}
public static ControllerType getTypeForHID(HIDIdentifier hid) {
@ -40,18 +45,27 @@ public enum ControllerType {
typeMap = new HashMap<>();
try {
try (var hidDb = ControllerType.class.getResourceAsStream("/hiddb.json5")) {
var json = GSON.fromJson(new InputStreamReader(hidDb), JsonObject.class);
for (var type : ControllerType.values()) {
if (!json.has(type.name().toLowerCase())) continue;
List<IoSupplier<InputStream>> dbs = Minecraft.getInstance().getResourceManager().listPacks()
.map(pack -> pack.getResource(PackType.CLIENT_RESOURCES, hidDbLocation))
.filter(Objects::nonNull)
.toList();
var themeJson = json.getAsJsonObject(type.name().toLowerCase());
for (var supplier : dbs) {
try (var hidDb = supplier.get()) {
var json = GSON.fromJson(new InputStreamReader(hidDb), JsonArray.class);
for (var typeElement : json) {
var typeObject = typeElement.getAsJsonObject();
int vendorId = themeJson.get("vendor").getAsInt();
for (var productIdEntry : themeJson.getAsJsonArray("product")) {
int productId = productIdEntry.getAsInt();
typeMap.put(new HIDIdentifier(vendorId, productId), type);
ControllerType type = new ControllerType(typeObject.get("name").getAsString(), typeObject.get("identifier").getAsString());
int vendorId = typeObject.get("vendor").getAsInt();
for (var productIdEntry : typeObject.getAsJsonArray("product")) {
int productId = productIdEntry.getAsInt();
typeMap.put(new HIDIdentifier(vendorId, productId), type);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {

View File

@ -1,15 +1,16 @@
package dev.isxander.controlify.controller;
package dev.isxander.controlify.controller.gamepad;
import dev.isxander.yacl.api.NameableEnum;
import net.minecraft.network.chat.Component;
public enum ControllerTheme implements NameableEnum {
XBOX_ONE("xbox"),
public enum BuiltinGamepadTheme implements NameableEnum {
DEFAULT("default"),
XBOX_ONE("xbox_one"),
DUALSHOCK4("dualshock4");
private final String id;
ControllerTheme(String id) {
BuiltinGamepadTheme(String id) {
this.id = id;
}
@ -19,6 +20,6 @@ public enum ControllerTheme implements NameableEnum {
@Override
public Component getDisplayName() {
return Component.translatable("controlify.controller_theme." + name().toLowerCase());
return Component.translatable("controlify.controller_theme." + id().toLowerCase());
}
}

View File

@ -0,0 +1,35 @@
package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.ControllerConfig;
public class GamepadConfig extends ControllerConfig {
public float leftStickDeadzoneX = 0.2f;
public float leftStickDeadzoneY = 0.2f;
public float rightStickDeadzoneX = 0.2f;
public float rightStickDeadzoneY = 0.2f;
public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT;
@Override
public void setDeadzone(int axis, float deadzone) {
switch (axis) {
case 0 -> leftStickDeadzoneX = deadzone;
case 1 -> leftStickDeadzoneY = deadzone;
case 2 -> rightStickDeadzoneX = deadzone;
case 3 -> rightStickDeadzoneY = deadzone;
default -> throw new IllegalArgumentException("Unknown axis: " + axis);
}
}
@Override
public float getDeadzone(int axis) {
return switch (axis) {
case 0 -> leftStickDeadzoneX;
case 1 -> leftStickDeadzoneY;
case 2 -> rightStickDeadzoneX;
case 3 -> rightStickDeadzoneY;
default -> throw new IllegalArgumentException("Unknown axis: " + axis);
};
}
}

View File

@ -0,0 +1,56 @@
package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.AbstractController;
import org.hid4java.HidDevice;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWGamepadState;
public class GamepadController extends AbstractController<GamepadState, GamepadConfig> {
private GamepadState state = GamepadState.EMPTY;
private GamepadState prevState = GamepadState.EMPTY;
public GamepadController(int joystickId, HidDevice hidDevice) {
super(joystickId, hidDevice);
if (!GLFW.glfwJoystickIsGamepad(joystickId))
throw new IllegalArgumentException("Joystick " + joystickId + " is not a gamepad!");
if (!this.name.startsWith(type().friendlyName()))
setName(GLFW.glfwGetGamepadName(joystickId));
this.defaultConfig = new GamepadConfig();
this.config = new GamepadConfig();
}
@Override
public GamepadState state() {
return state;
}
@Override
public GamepadState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
GamepadState.AxesState rawAxesState = GamepadState.AxesState.fromController(this);
GamepadState.AxesState axesState = rawAxesState
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY)
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY);
GamepadState.ButtonState buttonState = GamepadState.ButtonState.fromController(this);
state = new GamepadState(axesState, rawAxesState, buttonState);
}
public void consumeButtonState() {
this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY);
}
GLFWGamepadState getGamepadState() {
GLFWGamepadState state = GLFWGamepadState.create();
GLFW.glfwGetGamepadState(joystickId(), state);
return state;
}
}

View File

@ -0,0 +1,217 @@
package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.utils.ControllerUtils;
import org.lwjgl.glfw.GLFW;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class GamepadState implements ControllerState {
public static final GamepadState EMPTY = new GamepadState(AxesState.EMPTY, AxesState.EMPTY, ButtonState.EMPTY);
private final AxesState gamepadAxes;
private final AxesState rawGamepadAxes;
private final ButtonState gamepadButtons;
private final List<Float> unnamedAxes;
private final List<Float> unnamedRawAxes;
private final List<Boolean> unnamedButtons;
public GamepadState(AxesState gamepadAxes, AxesState rawGamepadAxes, ButtonState gamepadButtons) {
this.gamepadAxes = gamepadAxes;
this.rawGamepadAxes = rawGamepadAxes;
this.gamepadButtons = gamepadButtons;
this.unnamedAxes = List.of(
gamepadAxes.leftStickX(),
gamepadAxes.leftStickY(),
gamepadAxes.rightStickX(),
gamepadAxes.rightStickY(),
gamepadAxes.leftTrigger(),
gamepadAxes.rightTrigger()
);
this.unnamedRawAxes = List.of(
rawGamepadAxes.leftStickX(),
rawGamepadAxes.leftStickY(),
rawGamepadAxes.rightStickX(),
rawGamepadAxes.rightStickY(),
rawGamepadAxes.leftTrigger(),
rawGamepadAxes.rightTrigger()
);
this.unnamedButtons = List.of(
gamepadButtons.a(),
gamepadButtons.b(),
gamepadButtons.x(),
gamepadButtons.y(),
gamepadButtons.leftBumper(),
gamepadButtons.rightBumper(),
gamepadButtons.back(),
gamepadButtons.start(),
gamepadButtons.leftStick(),
gamepadButtons.rightStick(),
gamepadButtons.dpadUp(),
gamepadButtons.dpadDown(),
gamepadButtons.dpadLeft(),
gamepadButtons.dpadRight()
);
}
@Override
public List<Float> axes() {
return unnamedAxes;
}
@Override
public List<Float> rawAxes() {
return unnamedRawAxes;
}
@Override
public List<Boolean> buttons() {
return unnamedButtons;
}
@Override
public boolean hasAnyInput() {
return !this.gamepadAxes().equals(AxesState.EMPTY) || !this.gamepadButtons().equals(ButtonState.EMPTY);
}
public AxesState gamepadAxes() {
return gamepadAxes;
}
public AxesState rawGamepadAxes() {
return rawGamepadAxes;
}
public ButtonState gamepadButtons() {
return gamepadButtons;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (GamepadState) obj;
return Objects.equals(this.gamepadAxes, that.gamepadAxes) &&
Objects.equals(this.rawGamepadAxes, that.rawGamepadAxes) &&
Objects.equals(this.gamepadButtons, that.gamepadButtons);
}
@Override
public int hashCode() {
return Objects.hash(gamepadAxes, rawGamepadAxes, gamepadButtons);
}
@Override
public String toString() {
return "GamepadState[" +
"gamepadAxes=" + gamepadAxes + ", " +
"rawGamepadAxes=" + rawGamepadAxes + ", " +
"gamepadButtons=" + gamepadButtons + ']';
}
public record AxesState(
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger
) {
public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0);
public AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) {
return new AxesState(
ControllerUtils.deadzone(leftStickX, deadZoneX),
ControllerUtils.deadzone(leftStickY, deadZoneY),
rightStickX, rightStickY, leftTrigger, rightTrigger
);
}
public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) {
return new AxesState(
leftStickX, leftStickY,
ControllerUtils.deadzone(rightStickX, deadZoneX),
ControllerUtils.deadzone(rightStickY, deadZoneY),
leftTrigger, rightTrigger
);
}
public AxesState leftTriggerDeadZone(float deadZone) {
return new AxesState(
leftStickX, leftStickY, rightStickX, rightStickY,
ControllerUtils.deadzone(leftTrigger, deadZone),
rightTrigger
);
}
public AxesState rightTriggerDeadZone(float deadZone) {
return new AxesState(
leftStickX, leftStickY, rightStickX, rightStickY,
leftTrigger,
ControllerUtils.deadzone(rightTrigger, deadZone)
);
}
public static AxesState fromController(GamepadController controller) {
if (controller == null)
return EMPTY;
var state = controller.getGamepadState();
var axes = state.axes();
float leftX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X);
float leftY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y);
float rightX = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X);
float rightY = axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y);
float leftTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER) + 1f) / 2f;
float rightTrigger = (axes.get(GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) + 1f) / 2f;
return new AxesState(leftX, leftY, rightX, rightY, leftTrigger, rightTrigger);
}
}
public record ButtonState(
boolean a, boolean b, boolean x, boolean y,
boolean leftBumper, boolean rightBumper,
boolean back, boolean start, boolean guide,
boolean dpadUp, boolean dpadDown, boolean dpadLeft, boolean dpadRight,
boolean leftStick, boolean rightStick
) {
public static ButtonState EMPTY = new ButtonState(
false, false, false, false,
false, false,
false, false, false,
false, false, false, false,
false, false
);
public static ButtonState fromController(GamepadController controller) {
if (controller == null)
return EMPTY;
var state = controller.getGamepadState();
var buttons = state.buttons();
boolean a = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_A) == GLFW.GLFW_PRESS;
boolean b = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_B) == GLFW.GLFW_PRESS;
boolean x = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_X) == GLFW.GLFW_PRESS;
boolean y = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_Y) == GLFW.GLFW_PRESS;
boolean leftBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == GLFW.GLFW_PRESS;
boolean rightBumper = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == GLFW.GLFW_PRESS;
boolean back = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_BACK) == GLFW.GLFW_PRESS;
boolean start = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_START) == GLFW.GLFW_PRESS;
boolean guide = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_GUIDE) == GLFW.GLFW_PRESS;
boolean dpadUp = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW.GLFW_PRESS;
boolean dpadDown = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW.GLFW_PRESS;
boolean dpadLeft = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW.GLFW_PRESS;
boolean dpadRight = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW.GLFW_PRESS;
boolean leftStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_LEFT_THUMB) == GLFW.GLFW_PRESS;
boolean rightStick = buttons.get(GLFW.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB) == GLFW.GLFW_PRESS;
return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick);
}
}
}

View File

@ -57,7 +57,12 @@ public class ControllerHIDService implements HidServicesListener {
if (isController(device)) {
if (deviceQueue.peek() != null) {
deviceQueue.poll().accept(device);
try {
deviceQueue.poll().accept(device);
} catch (Throwable e) {
Controlify.LOGGER.error("Failed to handle controller device attach event.", e);
}
} else {
Controlify.LOGGER.error("Unhandled controller: " + ControllerType.getTypeForHID(new HIDIdentifier(device.getVendorId(), device.getProductId())).friendlyName());
}

View File

@ -0,0 +1,41 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerConfig;
import java.util.HashMap;
import java.util.Map;
public class JoystickConfig extends ControllerConfig {
private final Map<String, Float> deadzones;
private transient JoystickController controller;
public JoystickConfig(JoystickController controller) {
this.controller = controller;
deadzones = new HashMap<>();
for (int i = 0; i < controller.axisCount(); i++) {
if (controller.mapping().axis(i).requiresDeadzone())
deadzones.put(controller.mapping().axis(i).identifier(), 0.2f);
}
}
@Override
public void setDeadzone(int axis, float deadzone) {
if (axis < 0)
throw new IllegalArgumentException("Axis cannot be negative!");
deadzones.put(controller.mapping().axis(axis).identifier(), deadzone);
}
@Override
public float getDeadzone(int axis) {
if (axis < 0)
throw new IllegalArgumentException("Axis cannot be negative!");
return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f);
}
void setController(JoystickController controller) {
this.controller = controller;
}
}

View File

@ -0,0 +1,69 @@
package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.joystick.mapping.DataJoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import org.hid4java.HidDevice;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
public class JoystickController extends AbstractController<JoystickState, JoystickConfig> {
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final int axisCount, buttonCount, hatCount;
private final JoystickMapping mapping;
public JoystickController(int joystickId, @Nullable HidDevice hidDevice) {
super(joystickId, hidDevice);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(DataJoystickMapping.fromType(type()));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
}
@Override
public JoystickState state() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
state = JoystickState.fromJoystick(this);
}
public JoystickMapping mapping() {
return mapping;
}
public int axisCount() {
return axisCount;
}
public int buttonCount() {
return buttonCount;
}
public int hatCount() {
return hatCount;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
super.setConfig(gson, json);
this.config.setController(this);
}
}

View File

@ -0,0 +1,146 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import dev.isxander.controlify.utils.ControllerUtils;
import dev.isxander.yacl.api.NameableEnum;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class JoystickState implements ControllerState {
public static final JoystickState EMPTY = new JoystickState(UnmappedJoystickMapping.INSTANCE, List.of(), List.of(), List.of(), List.of());
private final JoystickMapping mapping;
private final List<Float> axes;
private final List<Float> rawAxes;
private final List<Boolean> buttons;
private final List<HatState> hats;
private JoystickState(JoystickMapping mapping, List<Float> axes, List<Float> rawAxes, List<Boolean> buttons, List<HatState> hats) {
this.mapping = mapping;
this.axes = axes;
this.rawAxes = rawAxes;
this.buttons = buttons;
this.hats = hats;
}
@Override
public List<Float> axes() {
return axes;
}
@Override
public List<Float> rawAxes() {
return rawAxes;
}
@Override
public List<Boolean> buttons() {
return buttons;
}
public List<HatState> hats() {
return hats;
}
@Override
public boolean hasAnyInput() {
return IntStream.range(0, axes().size()).anyMatch(i -> !mapping.axis(i).isAxisResting(axes().get(i)))
|| buttons().stream().anyMatch(Boolean::booleanValue)
|| hats().stream().anyMatch(hat -> hat != HatState.CENTERED);
}
public static JoystickState fromJoystick(JoystickController joystick) {
FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystick.joystickId());
List<Float> axes = new ArrayList<>();
List<Float> rawAxes = new ArrayList<>();
if (axesBuffer != null) {
int i = 0;
while (axesBuffer.hasRemaining()) {
var axisMapping = joystick.mapping().axis(i);
var axis = axisMapping.modifyAxis(axesBuffer.get());
var deadzone = axisMapping.requiresDeadzone();
rawAxes.add(axis);
axes.add(deadzone ? ControllerUtils.deadzone(axis, joystick.config().getDeadzone(i)) : axis);
i++;
}
}
ByteBuffer buttonBuffer = GLFW.glfwGetJoystickButtons(joystick.joystickId());
List<Boolean> buttons = new ArrayList<>();
if (buttonBuffer != null) {
while (buttonBuffer.hasRemaining()) {
buttons.add(buttonBuffer.get() == GLFW.GLFW_PRESS);
}
}
ByteBuffer hatBuffer = GLFW.glfwGetJoystickHats(joystick.joystickId());
List<JoystickState.HatState> hats = new ArrayList<>();
if (hatBuffer != null) {
while (hatBuffer.hasRemaining()) {
var state = switch (hatBuffer.get()) {
case GLFW.GLFW_HAT_CENTERED -> JoystickState.HatState.CENTERED;
case GLFW.GLFW_HAT_UP -> JoystickState.HatState.UP;
case GLFW.GLFW_HAT_RIGHT -> JoystickState.HatState.RIGHT;
case GLFW.GLFW_HAT_DOWN -> JoystickState.HatState.DOWN;
case GLFW.GLFW_HAT_LEFT -> JoystickState.HatState.LEFT;
case GLFW.GLFW_HAT_RIGHT_UP -> JoystickState.HatState.RIGHT_UP;
case GLFW.GLFW_HAT_RIGHT_DOWN -> JoystickState.HatState.RIGHT_DOWN;
case GLFW.GLFW_HAT_LEFT_UP -> JoystickState.HatState.LEFT_UP;
case GLFW.GLFW_HAT_LEFT_DOWN -> JoystickState.HatState.LEFT_DOWN;
default -> throw new IllegalStateException("Unexpected value: " + hatBuffer.get());
};
hats.add(state);
}
}
return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats);
}
public enum HatState implements NameableEnum {
CENTERED,
UP,
RIGHT,
DOWN,
LEFT,
RIGHT_UP,
RIGHT_DOWN,
LEFT_UP,
LEFT_DOWN;
public boolean isCentered() {
return this == CENTERED;
}
public boolean isRight() {
return this == RIGHT || this == RIGHT_UP || this == RIGHT_DOWN;
}
public boolean isUp() {
return this == UP || this == RIGHT_UP || this == LEFT_UP;
}
public boolean isLeft() {
return this == LEFT || this == LEFT_UP || this == LEFT_DOWN;
}
public boolean isDown() {
return this == DOWN || this == RIGHT_DOWN || this == LEFT_DOWN;
}
@Override
public Component getDisplayName() {
return Component.translatable("controlify.hat_state." + this.name().toLowerCase());
}
}
}

View File

@ -0,0 +1,155 @@
package dev.isxander.controlify.controller.joystick.mapping;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import dev.isxander.controlify.controller.ControllerType;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.Vec2;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class DataJoystickMapping implements JoystickMapping {
private static final Gson gson = new Gson();
private final Map<Integer, AxisMapping> axisMappings;
private final Map<Integer, ButtonMapping> buttonMappings;
private final Map<Integer, HatMapping> hatMappings;
public DataJoystickMapping(JsonObject object, ControllerType type) {
axisMappings = new HashMap<>();
object.getAsJsonArray("axes").forEach(element -> {
var axis = element.getAsJsonObject();
List<Integer> ids = axis.getAsJsonArray("ids").asList().stream().map(JsonElement::getAsInt).toList();
Vec2 inpRange = null;
Vec2 outRange = null;
if (axis.has("range")) {
var rangeElement = axis.get("range");
if (rangeElement.isJsonArray()) {
var rangeArray = rangeElement.getAsJsonArray();
outRange = new Vec2(rangeArray.get(0).getAsFloat(), rangeArray.get(1).getAsFloat());
inpRange = new Vec2(-1, 1);
} else if (rangeElement.isJsonObject()) {
var rangeObject = rangeElement.getAsJsonObject();
var inpRangeArray = rangeObject.getAsJsonArray("in");
inpRange = new Vec2(inpRangeArray.get(0).getAsFloat(), inpRangeArray.get(1).getAsFloat());
var outRangeArray = rangeObject.getAsJsonArray("out");
outRange = new Vec2(outRangeArray.get(0).getAsFloat(), outRangeArray.get(1).getAsFloat());
}
}
var restState = axis.get("rest").getAsFloat();
var deadzone = axis.get("deadzone").getAsBoolean();
var identifier = axis.get("identifier").getAsString();
var axisNames = axis.getAsJsonArray("axis_names").asList().stream()
.map(JsonElement::getAsJsonArray)
.map(JsonArray::asList)
.map(list -> list.stream().map(JsonElement::getAsString).toList())
.toList();
for (var id : ids) {
axisMappings.put(id, new AxisMapping(ids, identifier, inpRange, outRange, restState, deadzone, type.identifier(), axisNames));
}
});
buttonMappings = new HashMap<>();
object.getAsJsonArray("buttons").forEach(element -> {
var button = element.getAsJsonObject();
buttonMappings.put(button.get("button").getAsInt(), new ButtonMapping(button.get("name").getAsString(), type.identifier()));
});
hatMappings = new HashMap<>();
object.getAsJsonArray("hats").forEach(element -> {
var hat = element.getAsJsonObject();
hatMappings.put(hat.get("hat").getAsInt(), new HatMapping(hat.get("name").getAsString(), type.identifier()));
});
}
@Override
public Axis axis(int axis) {
if (!axisMappings.containsKey(axis))
return UnmappedJoystickMapping.INSTANCE.axis(axis);
return axisMappings.get(axis);
}
@Override
public Button button(int button) {
if (!buttonMappings.containsKey(button))
return UnmappedJoystickMapping.INSTANCE.button(button);
return buttonMappings.get(button);
}
@Override
public Hat hat(int hat) {
if (!hatMappings.containsKey(hat))
return UnmappedJoystickMapping.INSTANCE.hat(hat);
return hatMappings.get(hat);
}
public static JoystickMapping fromType(ControllerType type) {
var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + type.identifier() + ".json"));
if (resource.isEmpty()) {
Controlify.LOGGER.warn("No joystick mapping found for controller: '" + type.identifier() + "'");
return UnmappedJoystickMapping.INSTANCE;
}
try (var reader = resource.get().openAsReader()) {
return new DataJoystickMapping(gson.fromJson(reader, JsonObject.class), type);
} catch (Exception e) {
Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + type.identifier() + "'", e);
return UnmappedJoystickMapping.INSTANCE;
}
}
private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restState, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis {
@Override
public float modifyAxis(float value) {
if (inpRange() == null || outRange() == null)
return value;
return (value + (outRange().x - inpRange().x)) / (inpRange().y - inpRange().x) * (outRange().y - outRange().x);
}
@Override
public boolean isAxisResting(float value) {
return value == restState();
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".axis." + identifier());
}
@Override
public Component getDirectionName(int axis, JoystickAxisBind.AxisDirection direction) {
var directionId = axisNames().get(ids.indexOf(axis)).get(direction.ordinal());
return Component.translatable("controlify.joystick_mapping." + typeId() + ".axis." + identifier() + "." + directionId);
}
}
private record ButtonMapping(String identifier, String typeId) implements Button {
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".button." + identifier());
}
}
private record HatMapping(String identifier, String typeId) implements Hat {
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".hat." + identifier());
}
}
}

View File

@ -0,0 +1,38 @@
package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import net.minecraft.network.chat.Component;
public interface JoystickMapping {
Axis axis(int axis);
Button button(int button);
Hat hat(int hat);
interface Axis {
String identifier();
Component name();
boolean requiresDeadzone();
float modifyAxis(float value);
boolean isAxisResting(float value);
Component getDirectionName(int axis, JoystickAxisBind.AxisDirection direction);
}
interface Button {
String identifier();
Component name();
}
interface Hat {
String identifier();
Component name();
}
}

View File

@ -0,0 +1,80 @@
package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import net.minecraft.network.chat.Component;
public class UnmappedJoystickMapping implements JoystickMapping {
public static final UnmappedJoystickMapping INSTANCE = new UnmappedJoystickMapping();
@Override
public Axis axis(int axis) {
return new UnmappedAxis(axis);
}
@Override
public Button button(int button) {
return new UnmappedButton(button);
}
@Override
public Hat hat(int hat) {
return new UnmappedHat(hat);
}
private record UnmappedAxis(int axis) implements Axis {
@Override
public String identifier() {
return "axis-" + axis;
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping.unmapped.axis", axis + 1);
}
@Override
public boolean requiresDeadzone() {
return true;
}
@Override
public float modifyAxis(float value) {
return value;
}
@Override
public boolean isAxisResting(float value) {
return value == 0;
}
@Override
public Component getDirectionName(int axis, JoystickAxisBind.AxisDirection direction) {
return Component.translatable("controlify.joystick_mapping.unmapped.axis_direction." + direction.name().toLowerCase());
}
}
private record UnmappedButton(int button) implements Button {
@Override
public String identifier() {
return "button-" + button;
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping.unmapped.button", button + 1);
}
}
private record UnmappedHat(int hat) implements Hat {
@Override
public String identifier() {
return "hat-" + hat;
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping.unmapped.hat", hat + 1);
}
}
}

View File

@ -45,12 +45,12 @@ public class ControlifyEvents {
@FunctionalInterface
public interface ControllerStateUpdate {
void onControllerStateUpdate(Controller controller);
void onControllerStateUpdate(Controller<?, ?> controller);
}
@FunctionalInterface
public interface ControllerBindRegistry {
void onRegisterControllerBinds(ControllerBindings bindings, Controller controller);
void onRegisterControllerBinds(ControllerBindings<?> bindings, Controller<?, ?> controller);
}
@FunctionalInterface

View File

@ -1,20 +0,0 @@
package dev.isxander.controlify.gui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.Bind;
import dev.isxander.controlify.controller.Controller;
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 centerY) {
RenderSystem.setShaderTexture(0, button.textureLocation(controller));
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
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) { }
}

View File

@ -0,0 +1,4 @@
package dev.isxander.controlify.gui;
public record DrawSize(int width, int height) {
}

View File

@ -11,12 +11,10 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import java.math.RoundingMode;
public class ControllerDeadzoneCalibrationScreen extends Screen {
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
protected final Controller controller;
protected final Controller<?, ?> controller;
private final Screen parent;
private MultiLineLabel waitLabel, infoLabel, completeLabel;
@ -26,7 +24,7 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
protected boolean calibrating = false, calibrated = false;
protected int calibrationTicks = 0;
public ControllerDeadzoneCalibrationScreen(Controller controller, Screen parent) {
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Screen parent) {
super(Component.translatable("controlify.calibration.title"));
this.controller = controller;
this.parent = parent;
@ -110,42 +108,20 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
}
private void useCurrentStateAsDeadzone() {
var rawAxes = controller.state().rawAxes();
var axes = controller.state().axes();
var minDeadzoneLS = Math.max(rawAxes.leftStickX(), rawAxes.leftStickY()) + 0.08f;
var deadzoneLS = (float)Mth.clamp(0.05 * Math.ceil(minDeadzoneLS / 0.05), 0, 0.95);
var minDeadzoneRS = Math.max(rawAxes.rightStickX(), rawAxes.rightStickY()) + 0.08f;
var deadzoneRS = (float)Mth.clamp(0.05 * Math.ceil(minDeadzoneRS / 0.05), 0, 0.95);
controller.config().leftStickDeadzone = deadzoneLS;
controller.config().rightStickDeadzone = deadzoneRS;
for (int i = 0; i < axes.size(); i++) {
var axis = axes.get(i);
var minDeadzone = axis + 0.08f;
controller.config().setDeadzone(i, (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95));
}
}
private boolean stateChanged() {
var amt = 0.0001f;
var lsX = controller.state().rawAxes().leftStickX();
var prevLsX = controller.prevState().rawAxes().leftStickX();
if (Math.abs(lsX - prevLsX) > amt)
return true;
var lsY = controller.state().rawAxes().leftStickY();
var prevLsY = controller.prevState().rawAxes().leftStickY();
if (Math.abs(lsY - prevLsY) > amt)
return true;
var rsX = controller.state().rawAxes().rightStickX();
var prevRsX = controller.prevState().rawAxes().rightStickX();
if (Math.abs(rsX - prevRsX) > amt)
return true;
var rsY = controller.state().rawAxes().rightStickY();
var prevRsY = controller.prevState().rawAxes().rightStickY();
if (Math.abs(rsY - prevRsY) > amt)
return true;
return false;
return controller.state().axes().stream()
.anyMatch(axis -> Math.abs(axis - controller.prevState().axes().get(controller.state().axes().indexOf(axis))) > amt);
}
@Override

View File

@ -6,12 +6,10 @@ import net.minecraft.client.player.Input;
import net.minecraft.client.player.LocalPlayer;
public class ControllerPlayerMovement extends Input {
private final Controller controller;
private final Controller<?, ?> controller;
private final LocalPlayer player;
private boolean shiftToggled = false;
public ControllerPlayerMovement(Controller controller, LocalPlayer player) {
public ControllerPlayerMovement(Controller<?, ?> controller, LocalPlayer player) {
this.controller = controller;
this.player = player;
}

View File

@ -7,12 +7,12 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.player.KeyboardInput;
public class InGameInputHandler {
private final Controller controller;
private final Controller<?, ?> controller;
private final Minecraft minecraft;
private double lookInputX, lookInputY;
public InGameInputHandler(Controller controller) {
public InGameInputHandler(Controller<?, ?> controller) {
this.controller = controller;
this.minecraft = Minecraft.getInstance();
@ -51,10 +51,12 @@ public class InGameInputHandler {
}
protected void handlePlayerLookInput() {
var axes = controller.state().axes();
var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state();
var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive()) {
lookInputX = axes.rightStickX() * Math.abs(axes.rightStickX()) * controller.config().horizontalLookSensitivity;
lookInputY = axes.rightStickY() * Math.abs(axes.rightStickY()) * controller.config().verticalLookSensitivity;
lookInputX = impulseX * Math.abs(impulseX) * controller.config().horizontalLookSensitivity;
lookInputY = impulseY * Math.abs(impulseY) * controller.config().verticalLookSensitivity;
} else {
lookInputX = lookInputY = 0;
}

View File

@ -10,5 +10,5 @@ import java.util.Optional;
@FunctionalInterface
public interface ButtonActionSupplier {
Optional<GuideAction> supply(Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, Controller controller);
Optional<GuideAction> supply(Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, Controller<?, ?> controller);
}

View File

@ -17,7 +17,7 @@ import net.minecraft.world.phys.*;
import java.util.*;
public class InGameButtonGuide implements ButtonGuideRegistry {
private final Controller controller;
private final Controller<?, ?> controller;
private final LocalPlayer player;
private final Minecraft minecraft = Minecraft.getInstance();
@ -26,7 +26,7 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
private final List<GuideAction> leftGuides = new ArrayList<>();
private final List<GuideAction> rightGuides = new ArrayList<>();
public InGameButtonGuide(Controller controller, LocalPlayer localPlayer) {
public InGameButtonGuide(Controller<?, ?> controller, LocalPlayer localPlayer) {
this.controller = controller;
this.player = localPlayer;

View File

@ -0,0 +1,18 @@
package dev.isxander.controlify.mixins.core;
import com.mojang.blaze3d.platform.GLX;
import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.LongSupplier;
@Mixin(value = GLX.class, remap = false)
public class GLXMixin {
@Inject(method = "_initGlfw", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwInit()Z", shift = At.Shift.BEFORE))
private static void addInitHints(CallbackInfoReturnable<LongSupplier> cir) {
GLFW.glfwInitHint(GLFW.GLFW_JOYSTICK_HAT_BUTTONS, GLFW.GLFW_FALSE);
}
}

View File

@ -25,9 +25,16 @@ public abstract class MinecraftMixin {
@Shadow public abstract ToastComponent getToasts();
@ModifyExpressionValue(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/ReloadableResourceManager;createReload(Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Ljava/util/List;)Lnet/minecraft/server/packs/resources/ReloadInstance;"))
private ReloadInstance onInputInitialized(ReloadInstance resourceReload) {
// Controllers need to be initialized extremely late due to the data-driven nature of controllers.
resourceReload.done().thenRun(() -> Controlify.instance().initializeControllers());
return resourceReload;
}
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;setup(J)V", shift = At.Shift.AFTER))
private void onInputInitialized(CallbackInfo ci) {
Controlify.instance().onInitializeInput();
Controlify.instance().initializeControlify();
}
@Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;turnPlayer()V"))

View File

@ -5,14 +5,14 @@ import dev.isxander.controlify.controller.Controller;
public interface ComponentProcessor {
ComponentProcessor EMPTY = new ComponentProcessor(){};
default boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller controller) {
default boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) {
return false;
}
default boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
default boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
return false;
}
default void onFocusGained(ScreenProcessor<?> screen, Controller controller) {
default void onFocusGained(ScreenProcessor<?> screen, Controller<?, ?> controller) {
}
}

View File

@ -23,7 +23,7 @@ public class ScreenProcessor<T extends Screen> {
ControlifyEvents.VIRTUAL_MOUSE_TOGGLED.register(this::onVirtualMouseToggled);
}
public void onControllerUpdate(Controller controller) {
public void onControllerUpdate(Controller<?, ?> controller) {
if (!Controlify.instance().virtualMouseHandler().isVirtualMouseEnabled()) {
handleComponentNavigation(controller);
handleButtons(controller);
@ -43,7 +43,7 @@ public class ScreenProcessor<T extends Screen> {
}
}
protected void handleComponentNavigation(Controller controller) {
protected void handleComponentNavigation(Controller<?, ?> controller) {
if (screen.getFocused() == null)
setInitialFocus();
@ -59,28 +59,17 @@ public class ScreenProcessor<T extends Screen> {
boolean repeatEventAvailable = ++lastMoved >= controller.config().screenRepeatNavigationDelay;
var axes = controller.state().axes();
var prevAxes = controller.prevState().axes();
var buttons = controller.state().buttons();
var prevButtons = controller.prevState().buttons();
var bindings = controller.bindings();
FocusNavigationEvent.ArrowNavigation event = null;
if (axes.leftStickX() > 0.5f && (repeatEventAvailable || prevAxes.leftStickX() <= 0.5f)) {
if (bindings.GUI_NAVI_RIGHT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_RIGHT.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT);
} else if (axes.leftStickX() < -0.5f && (repeatEventAvailable || prevAxes.leftStickX() >= -0.5f)) {
} else if (bindings.GUI_NAVI_LEFT.held() && (repeatEventAvailable || !bindings.GUI_NAVI_LEFT.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT);
} else if (axes.leftStickY() < -0.5f && (repeatEventAvailable || prevAxes.leftStickY() >= -0.5f)) {
} else if (bindings.GUI_NAVI_UP.held() && (repeatEventAvailable || !bindings.GUI_NAVI_UP.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.UP);
} else if (axes.leftStickY() > 0.5f && (repeatEventAvailable || prevAxes.leftStickY() <= 0.5f)) {
} else if (bindings.GUI_NAVI_DOWN.held() && (repeatEventAvailable || !bindings.GUI_NAVI_DOWN.prevHeld())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN);
} else if (buttons.dpadUp() && (repeatEventAvailable || !prevButtons.dpadUp())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.UP);
} else if (buttons.dpadDown() && (repeatEventAvailable || !prevButtons.dpadDown())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.DOWN);
} else if (buttons.dpadLeft() && (repeatEventAvailable || !prevButtons.dpadLeft())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.LEFT);
} else if (buttons.dpadRight() && (repeatEventAvailable || !prevButtons.dpadRight())) {
event = accessor.invokeCreateArrowEvent(ScreenDirection.RIGHT);
}
if (event != null) {
@ -97,7 +86,7 @@ public class ScreenProcessor<T extends Screen> {
}
}
protected void handleButtons(Controller controller) {
protected void handleButtons(Controller<?, ?> controller) {
var focusTree = getFocusTree();
while (!focusTree.isEmpty()) {
var focused = focusTree.poll();
@ -111,7 +100,7 @@ public class ScreenProcessor<T extends Screen> {
screen.onClose();
}
protected void handleVMouseNavigation(Controller controller) {
protected void handleVMouseNavigation(Controller<?, ?> controller) {
}

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.AbstractButton;
@ -14,7 +14,7 @@ public class AbstractButtonComponentProcessor implements ComponentProcessor {
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (controller.bindings().GUI_PRESS.justPressed()) {
button.playDownSound(Minecraft.getInstance().getSoundManager());
button.onPress();

View File

@ -1,7 +1,7 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.CreativeModeInventoryScreenAccessor;
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
import net.minecraft.world.item.CreativeModeTabs;
@ -12,7 +12,7 @@ public class CreativeModeInventoryScreenProcessor extends ScreenProcessor<Creati
}
@Override
protected void handleVMouseNavigation(Controller controller) {
protected void handleVMouseNavigation(Controller<?, ?> controller) {
var accessor = (CreativeModeInventoryScreenAccessor) screen;
if (controller.bindings().GUI_NEXT_TAB.justPressed()) {

View File

@ -1,7 +1,7 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList;
@ -15,7 +15,7 @@ public class JoinMultiplayerScreenProcessor extends ScreenProcessor<JoinMultipla
}
@Override
protected void handleButtons(Controller controller) {
protected void handleButtons(Controller<?, ?> controller) {
if (screen.getFocused() instanceof Button && controller.bindings().GUI_BACK.justPressed()) {
screen.setFocused(list);
}

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.OptionsSubScreenAccessor;
import net.minecraft.client.Minecraft;
@ -14,7 +14,7 @@ public class LanguageSelectionListComponentProcessor implements ComponentProcess
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (controller.bindings().GUI_PRESS.justPressed()) {
var minecraft = Minecraft.getInstance();
var languageManager = minecraft.getLanguageManager();

View File

@ -1,7 +1,7 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.SelectWorldScreenAccessor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
@ -12,7 +12,7 @@ public class SelectWorldScreenProcessor extends ScreenProcessor<SelectWorldScree
}
@Override
protected void handleButtons(Controller controller) {
protected void handleButtons(Controller<?, ?> controller) {
if (screen.getFocused() != null && screen.getFocused() instanceof Button) {
if (controller.bindings().GUI_BACK.justPressed()) {
screen.setFocused(((SelectWorldScreenAccessor) screen).getList());

View File

@ -1,13 +1,13 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.JoinMultiplayerScreenAccessor;
public class ServerSelectionListEntryComponentProcessor implements ComponentProcessor {
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (controller.bindings().GUI_PRESS.justPressed()) {
screen.screen.setFocused(((JoinMultiplayerScreenAccessor) screen.screen).getSelectButton());
return true;

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import net.minecraft.client.gui.components.AbstractSliderButton;
import org.lwjgl.glfw.GLFW;
@ -25,21 +25,19 @@ public class SliderComponentProcessor implements ComponentProcessor {
}
@Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (!this.canChangeValueGetter.get()) return false;
var canSliderChange = ++lastSliderChange > SLIDER_CHANGE_DELAY;
var axes = controller.state().axes();
var buttons = controller.state().buttons();
if (axes.leftStickX() > 0.5f || buttons.dpadRight()) {
if (controller.bindings().GUI_NAVI_RIGHT.held()) {
if (canSliderChange) {
component.keyPressed(GLFW.GLFW_KEY_RIGHT, 0, 0);
lastSliderChange = 0;
}
return true;
} else if (axes.leftStickX() < -0.5f || buttons.dpadLeft()) {
} else if (controller.bindings().GUI_NAVI_LEFT.held()) {
if (canSliderChange) {
component.keyPressed(GLFW.GLFW_KEY_LEFT, 0, 0);
lastSliderChange = 0;
@ -52,7 +50,7 @@ public class SliderComponentProcessor implements ComponentProcessor {
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (!this.canChangeValueGetter.get()) return false;
if (controller.bindings().GUI_BACK.justPressed()) {
@ -64,7 +62,7 @@ public class SliderComponentProcessor implements ComponentProcessor {
}
@Override
public void onFocusGained(ScreenProcessor<?> screen, Controller controller) {
public void onFocusGained(ScreenProcessor<?> screen, Controller<?, ?> controller) {
System.out.println("navigated!");
this.canChangeValueSetter.accept(false);
}

View File

@ -1,14 +1,14 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.SelectWorldScreenAccessor;
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
public class WorldListEntryComponentProcessor implements ComponentProcessor {
@Override
public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
if (controller.bindings().GUI_PRESS.justPressed()) {
var selectWorldScreen = (SelectWorldScreen) screen.screen;
selectWorldScreen.setFocused(((SelectWorldScreenAccessor) selectWorldScreen).getSelectButton());

View File

@ -1,33 +1,44 @@
package dev.isxander.controlify.screenop.compat.yacl;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.yacl.gui.controllers.cycling.CyclingControllerElement;
public class CyclingControllerElementComponentProcessor implements ComponentProcessor {
private final CyclingControllerElement cyclingController;
private int lastInput = 0;
private boolean prevLeft, prevRight;
public CyclingControllerElementComponentProcessor(CyclingControllerElement cyclingController) {
this.cyclingController = cyclingController;
}
@Override
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller controller) {
var rightStickX = controller.state().axes().rightStickX();
var rightStickY = controller.state().axes().rightStickY();
var input = Math.abs(rightStickX) > Math.abs(rightStickY) ? rightStickX : rightStickY;
public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) {
var left = controller.bindings().GUI_NAVI_LEFT.held();
var right = controller.bindings().GUI_NAVI_RIGHT.held();
var inputI = Math.abs(input) < controller.config().buttonActivationThreshold ? 0 : input > 0 ? 1 : -1;
if (inputI != 0 && inputI != lastInput) {
cyclingController.cycleValue(input > 0 ? 1 : -1);
if (left && !prevLeft) {
prevLeft = true;
prevRight = false;
cyclingController.cycleValue(-1);
lastInput = inputI;
return true;
}
lastInput = inputI;
} else if (right && !prevRight) {
prevLeft = false;
prevRight = true;
return false;
cyclingController.cycleValue(1);
return true;
} else {
prevLeft = left;
prevRight = right;
return false;
}
}
}

View File

@ -1,35 +1,37 @@
package dev.isxander.controlify.screenop.compat.yacl;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.yacl.gui.controllers.slider.SliderControllerElement;
public class SliderControllerElementComponentProcessor implements ComponentProcessor {
private final SliderControllerElement slider;
private int ticksSinceIncrement = 0;
private int lastInput = 0;
private boolean prevLeft, prevRight;
public SliderControllerElementComponentProcessor(SliderControllerElement element) {
this.slider = element;
}
@Override
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller controller) {
public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
ticksSinceIncrement++;
var rightStickX = controller.state().axes().rightStickX();
var rightStickY = controller.state().axes().rightStickY();
var input = Math.abs(rightStickX) > Math.abs(rightStickY) ? rightStickX : rightStickY;
var left = controller.bindings().GUI_NAVI_LEFT.held();
var right = controller.bindings().GUI_NAVI_RIGHT.held();
if (Math.abs(input) > controller.config().buttonActivationThreshold) {
if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || input != lastInput) {
slider.incrementValue(lastInput = input > 0 ? 1 : -1);
if (left || right) {
if (ticksSinceIncrement > controller.config().screenRepeatNavigationDelay || left != prevLeft || right != prevRight) {
slider.incrementValue(left ? -1 : 1);
ticksSinceIncrement = 0;
prevLeft = left;
prevRight = right;
return true;
}
} else {
this.lastInput = 0;
this.prevLeft = false;
this.prevRight = false;
}
return false;

View File

@ -1,7 +1,7 @@
package dev.isxander.controlify.screenop.compat.yacl;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.yacl.gui.YACLScreen;
public class YACLScreenProcessor extends ScreenProcessor<YACLScreen> {
@ -10,7 +10,7 @@ public class YACLScreenProcessor extends ScreenProcessor<YACLScreen> {
}
@Override
protected void handleComponentNavigation(Controller controller) {
protected void handleComponentNavigation(Controller<?, ?> controller) {
if (controller.bindings().GUI_NEXT_TAB.justPressed()) {
var idx = screen.getCurrentCategoryIdx() + 1;
if (idx >= screen.config.categories().size()) idx = 0;

View File

@ -0,0 +1,7 @@
package dev.isxander.controlify.utils;
public class ControllerUtils {
public static float deadzone(float value, float deadzone) {
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
}
}

View File

@ -5,8 +5,8 @@ import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.datafixers.util.Pair;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.event.ControlifyEvents;
import dev.isxander.controlify.mixins.feature.virtualmouse.KeyboardHandlerAccessor;
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
@ -52,7 +52,7 @@ public class VirtualMouseHandler {
ControlifyEvents.INPUT_MODE_CHANGED.register(this::onInputModeChanged);
}
public void handleControllerInput(Controller controller) {
public void handleControllerInput(Controller<?, ?> controller) {
if (controller.bindings().VMOUSE_TOGGLE.justPressed()) {
toggleVirtualMouse();
}
@ -61,10 +61,10 @@ public class VirtualMouseHandler {
return;
}
var leftStickX = controller.state().axes().leftStickX();
var leftStickY = controller.state().axes().leftStickY();
var prevLeftStickX = controller.prevState().axes().leftStickX();
var prevLeftStickY = controller.prevState().axes().leftStickY();
var impulseY = controller.bindings().VMOUSE_MOVE_DOWN.state() - controller.bindings().VMOUSE_MOVE_UP.state();
var impulseX = controller.bindings().VMOUSE_MOVE_RIGHT.state() - controller.bindings().VMOUSE_MOVE_LEFT.state();
var prevImpulseY = controller.bindings().VMOUSE_MOVE_DOWN.prevState() - controller.bindings().VMOUSE_MOVE_UP.prevState();
var prevImpulseX = controller.bindings().VMOUSE_MOVE_RIGHT.prevState() - controller.bindings().VMOUSE_MOVE_LEFT.prevState();
if (minecraft.screen != null && minecraft.screen instanceof ISnapBehaviour snapBehaviour) {
snapPoints = snapBehaviour.getSnapPoints();
@ -73,8 +73,8 @@ public class VirtualMouseHandler {
}
// if just released stick, snap to nearest snap point
if (leftStickX == 0 && leftStickY == 0) {
if ((prevLeftStickX != 0 || prevLeftStickY != 0))
if (impulseX == 0 && impulseY == 0) {
if ((prevImpulseX != 0 || prevImpulseY != 0))
snapToClosestPoint();
} else {
snapping = false;
@ -84,8 +84,8 @@ public class VirtualMouseHandler {
// quadratic function to make small movements smaller
// abs to keep sign
targetX += leftStickX * Mth.abs(leftStickX) * 20f * sensitivity;
targetY += leftStickY * Mth.abs(leftStickY) * 20f * sensitivity;
targetX += impulseX * Mth.abs(impulseX) * 20f * sensitivity;
targetY += impulseY * Mth.abs(impulseY) * 20f * sensitivity;
targetX = Mth.clamp(targetX, 0, minecraft.getWindow().getWidth());
targetY = Mth.clamp(targetY, 0, minecraft.getWindow().getHeight());