1
0
forked from Clones/Controlify

rewrite most of joystick mapping

This commit is contained in:
isXander
2023-04-11 11:03:07 +01:00
parent ff7e676eb5
commit d3fc0a946b
19 changed files with 535 additions and 217 deletions

View File

@ -21,6 +21,9 @@ import dev.isxander.controlify.utils.ToastUtils;
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
@ -78,12 +81,14 @@ public class Controlify implements ControlifyApi {
if (config().globalSettings().loadVibrationNatives)
SDL2NativesManager.initialise();
boolean dirtyControllerConfig = false;
// find already connected controllers
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
if (GLFW.glfwJoystickPresent(jid)) {
try {
var controller = Controller.createOrGet(jid, controllerHIDService.fetchType());
var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType());
if (controllerOpt.isEmpty()) continue;
var controller = controllerOpt.get();
LOGGER.info("Controller found: " + controller.name());
config().loadOrCreateControllerData(controller);
@ -93,7 +98,7 @@ public class Controlify implements ControlifyApi {
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false;
dirtyControllerConfig = true;
config().setDirty();
}
} catch (Exception e) {
LOGGER.error("Failed to initialize controller with jid " + jid, e);
@ -101,10 +106,6 @@ public class Controlify implements ControlifyApi {
}
}
if (dirtyControllerConfig) {
config().save();
}
checkCompoundJoysticks();
if (Controller.CONTROLLERS.isEmpty()) {
@ -113,6 +114,9 @@ public class Controlify implements ControlifyApi {
if (currentController() == Controller.DUMMY && config().isFirstLaunch()) {
this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
} else {
// setCurrentController saves config
config().saveIfDirty();
}
// listen for new controllers
@ -172,15 +176,10 @@ public class Controlify implements ControlifyApi {
for (var controller : Controller.CONTROLLERS.values()) {
if (!outOfFocus)
controller.updateState();
else {
controller.clearState();
controller.rumbleManager().clearEffects();
wrapControllerError(controller::updateState, "Updating controller state", controller);
else
wrapControllerError(controller::clearState, "Clearing controller state", controller);
}
controller.rumbleManager().tick();
}
ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state();
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
if (switchableController.state().hasAnyInput()) {
@ -189,13 +188,23 @@ public class Controlify implements ControlifyApi {
askSwitchToast.remove();
askSwitchToast = null;
}
switchableController.clearState();
switchableController = null;
state = ControllerState.EMPTY;
}
}
if (outOfFocus)
wrapControllerError(() -> tickController(currentController, outOfFocus), "Ticking current controller", currentController);
}
private void tickController(Controller<?, ?> controller, boolean outOfFocus) {
ControllerState state = controller.state();
if (outOfFocus) {
state = ControllerState.EMPTY;
controller.rumbleManager().clearEffects();
} else {
controller.rumbleManager().tick();
}
if (state.hasAnyInput())
this.setInputMode(InputMode.CONTROLLER);
@ -209,22 +218,31 @@ public class Controlify implements ControlifyApi {
);
this.setCurrentController(null);
consecutiveInputSwitches = 0;
}
if (currentController == null) {
this.setInputMode(InputMode.KEYBOARD_MOUSE);
return;
}
if (client.screen != null) {
ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController);
if (minecraft.screen != null) {
ScreenProcessorProvider.provide(minecraft.screen).onControllerUpdate(controller);
}
if (client.level != null) {
if (minecraft.level != null) {
this.inGameInputHandler().inputTick();
}
this.virtualMouseHandler().handleControllerInput(currentController);
this.virtualMouseHandler().handleControllerInput(controller);
ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(currentController);
ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(controller);
}
public static void wrapControllerError(Runnable runnable, String errorTitle, Controller<?, ?> controller) {
try {
runnable.run();
} catch (Throwable e) {
CrashReport crashReport = CrashReport.forThrowable(e, errorTitle);
CrashReportCategory category = crashReport.addCategory("Affected controller");
category.setDetail("Controller name", controller::name);
category.setDetail("Controller identification", () -> controller.type().toString());
category.setDetail("Controller type", () -> controller.getClass().getCanonicalName());
throw new ReportedException(crashReport);
}
}
public ControlifyConfig config() {
@ -232,14 +250,24 @@ public class Controlify implements ControlifyApi {
}
private void onControllerHotplugged(int jid) {
var controller = Controller.createOrGet(jid, controllerHIDService.fetchType());
var controllerOpt = Controller.createOrGet(jid, controllerHIDService.fetchType());
if (controllerOpt.isEmpty()) return;
var controller = controllerOpt.get();
LOGGER.info("Controller connected: " + controller.name());
config().loadOrCreateControllerData(currentController);
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false;
config().setDirty();
}
this.askToSwitchController(controller);
checkCompoundJoysticks();
config().saveIfDirty();
}
private void onControllerDisconnect(int jid) {
@ -304,7 +332,12 @@ public class Controlify implements ControlifyApi {
controller = Controller.DUMMY;
if (this.currentController == controller) return;
if (this.currentController != null)
this.currentController.close();
this.currentController = controller;
this.currentController.open();
if (switchableController == controller) {
switchableController = null;

View File

@ -8,6 +8,7 @@ import dev.isxander.controlify.api.bind.ControllerBindingBuilder;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.mixins.compat.fapi.KeyBindingRegistryImplAccessor;
import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor;
import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor;
@ -38,6 +39,7 @@ public class ControllerBindings<T extends ControllerState> {
public final ControllerBinding<T>
WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT,
LOOK_UP, LOOK_DOWN, LOOK_LEFT, LOOK_RIGHT,
GAMEPAD_GYRO_BUTTON,
JUMP, SNEAK,
ATTACK, USE,
SPRINT,
@ -110,6 +112,15 @@ public class ControllerBindings<T extends ControllerState> {
.defaultBind(GamepadBinds.RIGHT_STICK_RIGHT)
.category(MOVEMENT_CATEGORY)
.build());
if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) {
register(GAMEPAD_GYRO_BUTTON = ControllerBindingBuilder.create(controller)
.identifier("controlify", "gamepad_gyro_button")
.defaultBind(new EmptyBind<>())
.category(MOVEMENT_CATEGORY)
.build());
} else {
GAMEPAD_GYRO_BUTTON = null;
}
register(JUMP = ControllerBindingBuilder.create(controller)
.identifier("controlify", "jump")
.defaultBind(GamepadBinds.A_BUTTON)

View File

@ -42,8 +42,8 @@ public class JoystickAxisBind implements IBind<JoystickState> {
JoystickMapping mapping = joystick.mapping();
String type = joystick.type().identifier();
String axis = mapping.axis(axisIndex).identifier();
String direction = mapping.axis(axisIndex).getDirectionIdentifier(axisIndex, this.direction);
String axis = mapping.axes()[axisIndex].identifier();
String direction = mapping.axes()[axisIndex].getDirectionIdentifier(axisIndex, this.direction);
var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/axis_" + axis + "_" + direction + ".png");
RenderSystem.setShaderTexture(0, texture);

View File

@ -31,7 +31,7 @@ public class JoystickButtonBind implements IBind<JoystickState> {
@Override
public void draw(PoseStack matrices, int x, int centerY) {
String type = joystick.type().identifier();
String button = joystick.mapping().button(buttonIndex).identifier();
String button = joystick.mapping().buttons()[buttonIndex].identifier();
var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/button_" + button + ".png");
RenderSystem.setShaderTexture(0, texture);

View File

@ -33,18 +33,18 @@ public class JoystickHatBind implements IBind<JoystickState> {
@Override
public void draw(PoseStack matrices, int x, int centerY) {
String type = joystick.type().identifier();
String button = joystick.mapping().button(hatIndex).identifier();
String hat = joystick.mapping().hats()[hatIndex].identifier();
String direction = "centered";
if (hatState.isUp())
direction = "up";
else if (hatState.isDown())
direction = "down";
else if (hatState.isLeft())
direction = "strong";
direction = "left";
else if (hatState.isRight())
direction = "weak";
direction = "right";
var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/hat" + button + "_" + direction + ".png");
var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/hat" + hat + "_" + direction + ".png");
RenderSystem.setShaderTexture(0, texture);
RenderSystem.setShaderColor(1, 1, 1, 1);

View File

@ -11,8 +11,8 @@ import org.quiltmc.json5.JsonReader;
import java.io.IOException;
import java.util.*;
public record ControllerType(String friendlyName, String identifier) {
public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown");
public record ControllerType(String friendlyName, String identifier, boolean forceJoystick, boolean dontLoad) {
public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown", false, false);
private static Map<HIDIdentifier, ControllerType> typeMap = null;
private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5");
@ -47,6 +47,8 @@ public record ControllerType(String friendlyName, String identifier) {
while (reader.hasNext()) {
String friendlyName = null;
String identifier = null;
boolean forceJoystick = false;
boolean dontLoad = false;
Set<HIDIdentifier> hids = new HashSet<>();
reader.beginObject();
@ -77,6 +79,8 @@ public record ControllerType(String friendlyName, String identifier) {
}
reader.endArray();
}
case "force_joystick" -> forceJoystick = reader.nextBoolean();
case "dont_load" -> dontLoad = reader.nextBoolean();
default -> {
Controlify.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping...");
reader.skipValue();
@ -90,7 +94,7 @@ public record ControllerType(String friendlyName, String identifier) {
continue;
}
var type = new ControllerType(friendlyName, identifier);
var type = new ControllerType(friendlyName, identifier, forceJoystick, dontLoad);
for (var hid : hids) {
typeMap.put(hid, type);
}

View File

@ -39,7 +39,7 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
this.buttonCount = joystickIds.stream().mapToInt(this::getButtonCountForJoystick).sum();
this.hatCount = joystickIds.stream().mapToInt(this::getHatCountForJoystick).sum();
this.mapping = RPJoystickMapping.fromType(type());
this.mapping = RPJoystickMapping.fromType(this);
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);

View File

@ -9,7 +9,7 @@ import java.util.Optional;
public record CompoundJoystickInfo(Collection<String> joystickUids, String friendlyName) {
public ControllerType type() {
return new ControllerType(friendlyName, createUID(joystickUids));
return new ControllerType(friendlyName, createUID(joystickUids), true, false);
}
public boolean canBeUsed() {

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerConfig;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import org.apache.commons.lang3.Validate;
import java.util.HashMap;
@ -21,7 +22,7 @@ public class JoystickConfig extends ControllerConfig {
if (axis < 0)
throw new IllegalArgumentException("Axis cannot be negative!");
deadzones.put(controller.mapping().axis(axis).identifier(), deadzone);
deadzones.put(controller.mapping().axes()[axis].identifier(), deadzone);
}
@Override
@ -29,16 +30,18 @@ public class JoystickConfig extends ControllerConfig {
if (axis < 0)
throw new IllegalArgumentException("Axis cannot be negative!");
return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f);
return deadzones.getOrDefault(controller.mapping().axes()[axis].identifier(), 0.2f);
}
void setup(JoystickController<?> controller) {
this.controller = controller;
if (this.deadzones == null) {
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);
for (int i = 0; i < controller.mapping().axes().length; i++) {
JoystickMapping.Axis axis = controller.mapping().axes()[i];
if (axis.requiresDeadzone())
deadzones.put(axis.identifier(), 0.2f);
}
}
}

View File

@ -9,8 +9,11 @@ import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMappi
public interface JoystickController<T extends JoystickConfig> extends Controller<JoystickState, T> {
JoystickMapping mapping();
@Deprecated
int axisCount();
@Deprecated
int buttonCount();
@Deprecated
int hatCount();
@Override

View File

@ -1,8 +1,10 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.Controlify;
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.debug.DebugProperties;
import dev.isxander.controlify.utils.ControllerUtils;
import dev.isxander.yacl.api.NameableEnum;
import net.minecraft.network.chat.Component;
@ -11,12 +13,13 @@ import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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());
public static final JoystickState EMPTY = new JoystickState(UnmappedJoystickMapping.EMPTY, List.of(), List.of(), List.of(), List.of());
private final JoystickMapping mapping;
@ -54,7 +57,7 @@ public class JoystickState implements ControllerState {
@Override
public boolean hasAnyInput() {
return IntStream.range(0, axes().size()).anyMatch(i -> !mapping.axis(i).isAxisResting(axes().get(i)))
return IntStream.range(0, axes().size()).anyMatch(i -> !mapping.axes()[i].isAxisResting(axes().get(i)))
|| buttons().stream().anyMatch(Boolean::booleanValue)
|| hats().stream().anyMatch(hat -> hat != HatState.CENTERED);
}
@ -70,68 +73,85 @@ public class JoystickState implements ControllerState {
}
public static JoystickState fromJoystick(JoystickController<?> joystick, int joystickId) {
if (DebugProperties.PRINT_JOY_INPUT_COUNT)
Controlify.LOGGER.info("Printing joy input for " + joystick.name());
FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystickId);
List<Float> axes = new ArrayList<>();
List<Float> rawAxes = new ArrayList<>();
if (axesBuffer != null) {
float[] inAxes = new float[axesBuffer.limit()];
if (DebugProperties.PRINT_JOY_INPUT_COUNT)
Controlify.LOGGER.info("Axes count = " + inAxes.length);
{
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);
inAxes[i] = axesBuffer.get();
i++;
}
}
ByteBuffer buttonBuffer = GLFW.glfwGetJoystickButtons(joystickId);
List<Boolean> buttons = new ArrayList<>();
if (buttonBuffer != null) {
boolean[] inButtons = new boolean[buttonBuffer.limit()];
if (DebugProperties.PRINT_JOY_INPUT_COUNT)
Controlify.LOGGER.info("Button count = " + inButtons.length);
{
int i = 0;
while (buttonBuffer.hasRemaining()) {
buttons.add(buttonBuffer.get() == GLFW.GLFW_PRESS);
inButtons[i] = buttonBuffer.get() == GLFW.GLFW_PRESS;
i++;
}
}
ByteBuffer hatBuffer = GLFW.glfwGetJoystickHats(joystickId);
List<JoystickState.HatState> hats = new ArrayList<>();
if (hatBuffer != null) {
HatState[] inHats = new HatState[hatBuffer.limit()];
if (DebugProperties.PRINT_JOY_INPUT_COUNT)
Controlify.LOGGER.info("Hat count = " + inHats.length);
{
int i = 0;
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;
case GLFW.GLFW_HAT_CENTERED -> HatState.CENTERED;
case GLFW.GLFW_HAT_UP -> HatState.UP;
case GLFW.GLFW_HAT_RIGHT -> HatState.RIGHT;
case GLFW.GLFW_HAT_DOWN -> HatState.DOWN;
case GLFW.GLFW_HAT_LEFT -> HatState.LEFT;
case GLFW.GLFW_HAT_RIGHT_UP -> HatState.RIGHT_UP;
case GLFW.GLFW_HAT_RIGHT_DOWN -> HatState.RIGHT_DOWN;
case GLFW.GLFW_HAT_LEFT_UP -> HatState.LEFT_UP;
case GLFW.GLFW_HAT_LEFT_DOWN -> HatState.LEFT_DOWN;
default -> throw new IllegalStateException("Unexpected value: " + hatBuffer.get());
};
hats.add(state);
inHats[i] = state;
}
}
return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats);
JoystickMapping.JoystickData data = new JoystickMapping.JoystickData(inAxes, inButtons, inHats);
JoystickMapping mapping = joystick.mapping();
JoystickMapping.Axis[] axes = mapping.axes();
List<Float> rawAxes = new ArrayList<>(axes.length);
List<Float> deadzoneAxes = new ArrayList<>(axes.length);
for (int i = 0; i < axes.length; i++) {
var axis = axes[i];
float state = axis.getAxis(data);
rawAxes.add(state);
deadzoneAxes.add(axis.requiresDeadzone() ? ControllerUtils.deadzone(state, i) : state);
}
List<Boolean> buttons = Arrays.stream(mapping.buttons()).map(button -> button.isPressed(data)).toList();
List<HatState> hats = Arrays.stream(mapping.hats()).map(hat -> hat.getHatState(data)).toList();
return new JoystickState(joystick.mapping(), deadzoneAxes, rawAxes, buttons, hats);
}
public static JoystickState empty(JoystickController<?> joystick) {
var axes = new ArrayList<Float>();
var buttons = new ArrayList<Boolean>();
var hats = new ArrayList<HatState>();
for (int i = 0; i < joystick.axisCount(); i++) {
axes.add(joystick.mapping().axis(i).restingValue());
}
for (int i = 0; i < joystick.buttonCount(); i++) {
buttons.add(false);
}
for (int i = 0; i < joystick.hatCount(); i++) {
hats.add(HatState.CENTERED);
}
var axes = Arrays.stream(joystick.mapping().axes()).map(JoystickMapping.Axis::restingValue).toList();
var buttons = IntStream.range(0, joystick.mapping().buttons().length).mapToObj(i -> false).toList();
var hats = IntStream.range(0, joystick.mapping().hats().length).mapToObj(i -> HatState.CENTERED).toList();
return new JoystickState(joystick.mapping(), axes, axes, buttons, hats);
}

View File

@ -13,17 +13,12 @@ import java.util.Objects;
public class SingleJoystickController extends AbstractController<JoystickState, JoystickConfig> implements JoystickController<JoystickConfig> {
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final int axisCount, buttonCount, hatCount;
private final JoystickMapping mapping;
public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type()));
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(this));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
@ -57,17 +52,17 @@ public class SingleJoystickController extends AbstractController<JoystickState,
@Override
public int axisCount() {
return axisCount;
return mapping().axes().length;
}
@Override
public int buttonCount() {
return buttonCount;
return mapping.buttons().length;
}
@Override
public int hatCount() {
return hatCount;
return mapping.hats().length;
}
@Override

View File

@ -1,14 +1,13 @@
package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import dev.isxander.controlify.controller.joystick.JoystickState;
import net.minecraft.network.chat.Component;
public interface JoystickMapping {
Axis axis(int axis);
Button button(int button);
Hat hat(int hat);
Axis[] axes();
Button[] buttons();
Hat[] hats();
interface Axis {
String identifier();
@ -17,7 +16,7 @@ public interface JoystickMapping {
boolean requiresDeadzone();
float modifyAxis(float value);
float getAxis(JoystickData data);
boolean isAxisResting(float value);
@ -30,11 +29,18 @@ public interface JoystickMapping {
String identifier();
Component name();
boolean isPressed(JoystickData data);
}
interface Hat {
JoystickState.HatState getHatState(JoystickData data);
String identifier();
Component name();
}
record JoystickData(float[] axes, boolean[] buttons, JoystickState.HatState[] hats) {
}
}

View File

@ -1,123 +1,282 @@
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 dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.Vec2;
import org.jetbrains.annotations.Nullable;
import org.quiltmc.json5.JsonReader;
import org.quiltmc.json5.JsonToken;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.IOException;
import java.util.*;
public class RPJoystickMapping 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;
private final AxisMapping[] axes;
private final ButtonMapping[] buttons;
private final HatMapping[] hats;
public RPJoystickMapping(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();
public RPJoystickMapping(JsonReader reader, ControllerType type) throws IOException {
AxisMapping[] axes = null;
ButtonMapping[] buttons = null;
HatMapping[] hats = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "axes" -> {
if (axes != null)
throw new IllegalStateException("Axes defined twice.");
axes = readAxes(reader, type);
}
case "buttons" -> {
if (buttons != null)
throw new IllegalStateException("Buttons defined twice.");
buttons = readButtons(reader, type);
}
case "hats" -> {
if (hats != null)
throw new IllegalStateException("Hats defined twice.");
hats = readHats(reader, type);
}
default -> {
Controlify.LOGGER.warn("Unknown field in joystick mapping: " + name + ". Expected values: ['axes', 'buttons', 'hats']");
reader.skipValue();
}
}
}
reader.endObject();
this.axes = axes;
this.buttons = buttons;
this.hats = hats;
}
private AxisMapping[] readAxes(JsonReader reader, ControllerType type) throws IOException {
List<AxisMapping> axes = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
List<Integer> ids = new ArrayList<>();
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());
boolean deadzone = false;
float restState = 0f;
String identifier = null;
List<String[]> axisNames = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "ids" -> {
reader.beginArray();
while (reader.hasNext()) {
ids.add(reader.nextInt());
}
reader.endArray();
}
case "identifier" -> {
identifier = reader.nextString();
}
case "range" -> {
if (reader.peek() == JsonToken.BEGIN_ARRAY) {
reader.beginArray();
outRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble());
inpRange = new Vec2(-1, 1);
} else if (rangeElement.isJsonObject()) {
var rangeObject = rangeElement.getAsJsonObject();
reader.endArray();
} else {
reader.beginObject();
while (reader.hasNext()) {
String rangeName = reader.nextName();
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());
switch (rangeName) {
case "in" -> {
reader.beginArray();
inpRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble());
reader.endArray();
}
case "out" -> {
reader.beginArray();
outRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble());
reader.endArray();
}
default -> {
reader.skipValue();
Controlify.LOGGER.info("Unknown axis range property: " + rangeName + ". Expected are ['in', 'out']");
}
}
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();
}
reader.endObject();
}
}
case "rest" -> {
restState = (float) reader.nextDouble();
}
case "deadzone" -> {
deadzone = reader.nextBoolean();
}
case "axis_names" -> {
reader.beginArray();
while (reader.hasNext()) {
reader.beginArray();
axisNames.add(new String[] { reader.nextString(), reader.nextString() });
reader.endArray();
}
reader.endArray();
}
default -> {
reader.skipValue();
Controlify.LOGGER.info("Unknown axis property: " + name + ". Expected are ['identifier', 'axis_names', 'ids', 'range', 'rest', 'deadzone']");
}
}
}
reader.endObject();
for (var id : ids) {
axisMappings.put(id, new AxisMapping(ids, identifier, inpRange, outRange, restState, deadzone, type.identifier(), axisNames));
axes.add(new AxisMapping(id, identifier, inpRange, outRange, restState, deadzone, type.identifier(), axisNames.get(ids.indexOf(id))));
}
});
}
reader.endArray();
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()));
});
return axes.toArray(new AxisMapping[0]);
}
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()));
});
private ButtonMapping[] readButtons(JsonReader reader, ControllerType type) throws IOException {
List<ButtonMapping> buttons = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
int id = -1;
String btnName = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "button" -> id = reader.nextInt();
case "name" -> btnName = reader.nextString();
default -> {
reader.skipValue();
Controlify.LOGGER.info("Unknown button property: " + name + ". Expected are ['button', 'name']");
}
}
}
reader.endObject();
buttons.add(new ButtonMapping(id, btnName, type.identifier()));
}
reader.endArray();
return buttons.toArray(new ButtonMapping[0]);
}
private HatMapping[] readHats(JsonReader reader, ControllerType type) throws IOException {
List<HatMapping> hats = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
int id = -1;
String hatName = null;
HatMapping.EmulatedAxis axis = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "hat" -> id = reader.nextInt();
case "name" -> hatName = reader.nextString();
case "emulated_axis" -> {
int axisId = -1;
Map<Float, JoystickState.HatState> states = new HashMap<>();
reader.beginObject();
while (reader.hasNext()) {
String emulatedName = reader.nextName();
for (var hatState : JoystickState.HatState.values()) {
if (hatState.name().equalsIgnoreCase(emulatedName)) {
states.put((float) reader.nextDouble(), hatState);
}
}
if (emulatedName.equalsIgnoreCase("axis")) {
axisId = reader.nextInt();
}
}
reader.endObject();
if (axisId == -1) {
Controlify.LOGGER.error("No axis id defined for emulated hat " + hatName + "! Skipping.");
continue;
}
if (states.size() != JoystickState.HatState.values().length) {
Controlify.LOGGER.error("Not all hat states are defined for emulated hat " + hatName + "! Skipping.");
continue;
}
axis = new HatMapping.EmulatedAxis(axisId, states);
}
default -> {
reader.skipValue();
Controlify.LOGGER.info("Unknown hat property: " + name + ". Expected are ['hat', 'name']");
}
}
}
reader.endObject();
hats.add(new HatMapping(id, hatName, type.identifier(), axis));
}
reader.endArray();
return hats.toArray(new HatMapping[0]);
}
@Override
public Axis axis(int axis) {
if (!axisMappings.containsKey(axis))
return UnmappedJoystickMapping.INSTANCE.axis(axis);
return axisMappings.get(axis);
public Axis[] axes() {
return axes;
}
@Override
public Button button(int button) {
if (!buttonMappings.containsKey(button))
return UnmappedJoystickMapping.INSTANCE.button(button);
return buttonMappings.get(button);
public Button[] buttons() {
return buttons;
}
@Override
public Hat hat(int hat) {
if (!hatMappings.containsKey(hat))
return UnmappedJoystickMapping.INSTANCE.hat(hat);
return hatMappings.get(hat);
public Hat[] hats() {
return hats;
}
public static JoystickMapping fromType(ControllerType type) {
var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + type.identifier() + ".json"));
public static JoystickMapping fromType(JoystickController<?> joystick) {
var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + joystick.type().identifier() + ".json"));
if (resource.isEmpty()) {
Controlify.LOGGER.warn("No joystick mapping found for controller: '" + type.identifier() + "'");
return UnmappedJoystickMapping.INSTANCE;
Controlify.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().identifier() + "'");
return new UnmappedJoystickMapping(joystick.joystickId());
}
try (var reader = resource.get().openAsReader()) {
return new RPJoystickMapping(gson.fromJson(reader, JsonObject.class), type);
try (var reader = JsonReader.json5(resource.get().openAsReader())) {
return new RPJoystickMapping(reader, joystick.type());
} catch (Exception e) {
Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + type.identifier() + "'", e);
return UnmappedJoystickMapping.INSTANCE;
Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + joystick.type().identifier() + "'", e);
return new UnmappedJoystickMapping(joystick.joystickId());
}
}
private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis {
private record AxisMapping(int id, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, String[] axisNames) implements Axis {
@Override
public float modifyAxis(float value) {
if (inpRange() == null || outRange() == null)
return value;
public float getAxis(JoystickData data) {
float rawAxis = data.axes()[id];
return (value + (outRange().x - inpRange().x)) / (inpRange().y - inpRange().x) * (outRange().y - outRange().x);
if (inpRange() == null || outRange() == null)
return rawAxis;
return (rawAxis + (outRange().x - inpRange().x)) / (inpRange().y - inpRange().x) * (outRange().y - outRange().x);
}
@Override
@ -132,23 +291,40 @@ public class RPJoystickMapping implements JoystickMapping {
@Override
public String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction) {
return this.axisNames().get(ids.indexOf(axis)).get(direction.ordinal());
return this.axisNames()[direction.ordinal()];
}
}
private record ButtonMapping(String identifier, String typeId) implements Button {
private record ButtonMapping(int id, String identifier, String typeId) implements Button {
@Override
public boolean isPressed(JoystickData data) {
return data.buttons()[id];
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".button." + identifier());
}
}
private record HatMapping(String identifier, String typeId) implements Hat {
private record HatMapping(int hatId, String identifier, String typeId, @Nullable EmulatedAxis emulatedAxis) implements Hat {
@Override
public JoystickState.HatState getHatState(JoystickData data) {
if (emulatedAxis() != null) {
var axis = emulatedAxis();
var axisValue = data.axes()[axis.axisId()];
return emulatedAxis().states().get(axisValue);
}
return data.hats()[hatId()];
}
@Override
public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".hat." + identifier());
}
private record EmulatedAxis(int axisId, Map<Float, JoystickState.HatState> states) {
}
}
}

View File

@ -1,27 +1,65 @@
package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.Arrays;
public class UnmappedJoystickMapping implements JoystickMapping {
public static final UnmappedJoystickMapping INSTANCE = new UnmappedJoystickMapping();
public static final UnmappedJoystickMapping EMPTY = new UnmappedJoystickMapping(0, 0, 0);
@Override
public Axis axis(int axis) {
return new UnmappedAxis(axis);
private final UnmappedAxis[] axes;
private final UnmappedButton[] buttons;
private final UnmappedHat[] hats;
private UnmappedJoystickMapping(int axisCount, int buttonCount, int hatCount) {
this.axes = new UnmappedAxis[axisCount];
for (int i = 0; i < axisCount; i++) {
this.axes[i] = new UnmappedAxis(i);
}
this.buttons = new UnmappedButton[axisCount];
for (int i = 0; i < buttonCount; i++) {
this.buttons[i] = new UnmappedButton(i);
}
this.hats = new UnmappedHat[hatCount];
for (int i = 0; i < hatCount; i++) {
this.hats[i] = new UnmappedHat(i);
}
}
public UnmappedJoystickMapping(int joystickId) {
this(
GLFW.glfwGetJoystickAxes(joystickId).limit(),
GLFW.glfwGetJoystickButtons(joystickId).limit(),
GLFW.glfwGetJoystickHats(joystickId).limit()
);
}
@Override
public Button button(int button) {
return new UnmappedButton(button);
public Axis[] axes() {
return axes;
}
@Override
public Hat hat(int hat) {
return new UnmappedHat(hat);
public Button[] buttons() {
return buttons;
}
@Override
public Hat[] hats() {
return hats;
}
private record UnmappedAxis(int axis) implements Axis {
@Override
public float getAxis(JoystickData data) {
return data.axes()[axis];
}
@Override
public String identifier() {
@ -38,11 +76,6 @@ public class UnmappedJoystickMapping implements JoystickMapping {
return true;
}
@Override
public float modifyAxis(float value) {
return value;
}
@Override
public boolean isAxisResting(float value) {
return value == restingValue();
@ -60,6 +93,11 @@ public class UnmappedJoystickMapping implements JoystickMapping {
}
private record UnmappedButton(int button) implements Button {
@Override
public boolean isPressed(JoystickData data) {
return data.buttons()[button];
}
@Override
public String identifier() {
return "button-" + button;
@ -72,6 +110,11 @@ public class UnmappedJoystickMapping implements JoystickMapping {
}
private record UnmappedHat(int hat) implements Hat {
@Override
public JoystickState.HatState getHatState(JoystickData data) {
return data.hats()[hat];
}
@Override
public String identifier() {
return "hat-" + hat;

View File

@ -1,12 +1,19 @@
package dev.isxander.controlify.debug;
public class DebugProperties {
// Renders debug overlay for vmouse snapping
public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false);
// Forces all gamepads to be treated as a regular joystick
public static final boolean FORCE_JOYSTICK = boolProp("controlify.debug.force_joystick", false);
import net.fabricmc.loader.api.FabricLoader;
private static boolean boolProp(String name, boolean def) {
public class DebugProperties {
/* Renders debug overlay for vmouse snapping */
public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false, false);
/* Forces all gamepads to be treated as a regular joystick */
public static final boolean FORCE_JOYSTICK = boolProp("controlify.debug.force_joystick", false, false);
/* Prints joystick input counts for making joystick mappings */
public static final boolean PRINT_JOY_INPUT_COUNT = boolProp("controlify.debug.print_joy_input_count", false, true);
/* Print gyro data if supported */
public static final boolean PRINT_GYRO = boolProp("controlify.debug.print_gyro", false, false);
private static boolean boolProp(String name, boolean defProd, boolean defDev) {
boolean def = FabricLoader.getInstance().isDevelopmentEnvironment() ? defDev : defProd;
return Boolean.parseBoolean(System.getProperty(name, Boolean.toString(def)));
}
}

View File

@ -53,6 +53,8 @@ public class MultiPlayerGameModeMixin {
}
private void startRumble(BlockState state) {
stopRumble();
var effect = ContinuousRumbleEffect.builder()
.byTick(tick -> new RumbleState(
0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f,

View File

@ -1,9 +1,12 @@
package dev.isxander.controlify.rumble;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import org.apache.commons.lang3.Validate;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
public final class BasicRumbleEffect implements RumbleEffect {
@ -11,6 +14,7 @@ public final class BasicRumbleEffect implements RumbleEffect {
private int tick = 0;
private boolean finished;
private int priority = 0;
private BooleanSupplier earlyFinishCondition = () -> false;
public BasicRumbleEffect(RumbleState[] keyframes) {
this.keyframes = keyframes;
@ -19,7 +23,7 @@ public final class BasicRumbleEffect implements RumbleEffect {
@Override
public void tick() {
tick++;
if (tick >= keyframes.length)
if (tick >= keyframes.length || earlyFinishCondition.getAsBoolean())
finished = true;
}
@ -55,6 +59,12 @@ public final class BasicRumbleEffect implements RumbleEffect {
return keyframes;
}
public BasicRumbleEffect earlyFinish(BooleanSupplier condition) {
var current = earlyFinishCondition;
this.earlyFinishCondition = () -> current.getAsBoolean() || condition.getAsBoolean();
return this;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
@ -76,6 +86,22 @@ public final class BasicRumbleEffect implements RumbleEffect {
"priority=" + this.priority() + ']';
}
public BasicRumbleEffect join(BasicRumbleEffect other) {
return BasicRumbleEffect.join(this, other);
}
public BasicRumbleEffect repeat(int count) {
Validate.isTrue(count > 0, "count must be greater than 0");
if (count == 1) return this;
BasicRumbleEffect effect = this;
for (int i = 0; i < count - 1; i++) {
effect = BasicRumbleEffect.join(effect, this);
}
return effect;
}
/**
* Creates a rumble effect where the state is determined by the tick.
*
@ -133,19 +159,8 @@ public final class BasicRumbleEffect implements RumbleEffect {
return new BasicRumbleEffect(states);
}
public BasicRumbleEffect join(BasicRumbleEffect other) {
return BasicRumbleEffect.join(this, other);
}
public BasicRumbleEffect repeat(int count) {
Validate.isTrue(count > 0, "count must be greater than 0");
if (count == 1) return this;
BasicRumbleEffect effect = this;
for (int i = 0; i < count - 1; i++) {
effect = BasicRumbleEffect.join(effect, this);
}
return effect;
public static BooleanSupplier finishOnScreenChange() {
Screen screen = Minecraft.getInstance().screen;
return () -> screen != Minecraft.getInstance().screen;
}
}

View File

@ -31,7 +31,7 @@
"fabricloader": ">=0.14.0",
"minecraft": "~1.19.4",
"java": ">=17",
"yet-another-config-lib": ">=2.4.0"
"yet-another-config-lib": "^2.4.0"
},
"breaks": {
"midnightcontrols": "*"