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

@ -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);
}
}
}