forked from Clones/Controlify
joystick support
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user