forked from Clones/Controlify
joystick support
This commit is contained in:
@ -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