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 dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader; 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.Minecraft;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@ -78,12 +81,14 @@ public class Controlify implements ControlifyApi {
if (config().globalSettings().loadVibrationNatives) if (config().globalSettings().loadVibrationNatives)
SDL2NativesManager.initialise(); SDL2NativesManager.initialise();
boolean dirtyControllerConfig = false;
// find already connected controllers // find already connected controllers
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) { for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
if (GLFW.glfwJoystickPresent(jid)) { if (GLFW.glfwJoystickPresent(jid)) {
try { 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()); LOGGER.info("Controller found: " + controller.name());
config().loadOrCreateControllerData(controller); config().loadOrCreateControllerData(controller);
@ -93,7 +98,7 @@ public class Controlify implements ControlifyApi {
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) { if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false; controller.config().allowVibrations = false;
dirtyControllerConfig = true; config().setDirty();
} }
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Failed to initialize controller with jid " + jid, 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(); checkCompoundJoysticks();
if (Controller.CONTROLLERS.isEmpty()) { if (Controller.CONTROLLERS.isEmpty()) {
@ -113,6 +114,9 @@ public class Controlify implements ControlifyApi {
if (currentController() == Controller.DUMMY && config().isFirstLaunch()) { if (currentController() == Controller.DUMMY && config().isFirstLaunch()) {
this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
} else {
// setCurrentController saves config
config().saveIfDirty();
} }
// listen for new controllers // listen for new controllers
@ -172,15 +176,10 @@ public class Controlify implements ControlifyApi {
for (var controller : Controller.CONTROLLERS.values()) { for (var controller : Controller.CONTROLLERS.values()) {
if (!outOfFocus) if (!outOfFocus)
controller.updateState(); wrapControllerError(controller::updateState, "Updating controller state", controller);
else { else
controller.clearState(); wrapControllerError(controller::clearState, "Clearing controller state", controller);
controller.rumbleManager().clearEffects();
} }
controller.rumbleManager().tick();
}
ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state();
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) { if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
if (switchableController.state().hasAnyInput()) { if (switchableController.state().hasAnyInput()) {
@ -189,13 +188,23 @@ public class Controlify implements ControlifyApi {
askSwitchToast.remove(); askSwitchToast.remove();
askSwitchToast = null; askSwitchToast = null;
} }
switchableController.clearState();
switchableController = null; 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; state = ControllerState.EMPTY;
controller.rumbleManager().clearEffects();
} else {
controller.rumbleManager().tick();
}
if (state.hasAnyInput()) if (state.hasAnyInput())
this.setInputMode(InputMode.CONTROLLER); this.setInputMode(InputMode.CONTROLLER);
@ -209,22 +218,31 @@ public class Controlify implements ControlifyApi {
); );
this.setCurrentController(null); this.setCurrentController(null);
consecutiveInputSwitches = 0; consecutiveInputSwitches = 0;
}
if (currentController == null) {
this.setInputMode(InputMode.KEYBOARD_MOUSE);
return; return;
} }
if (client.screen != null) { if (minecraft.screen != null) {
ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController); ScreenProcessorProvider.provide(minecraft.screen).onControllerUpdate(controller);
} }
if (client.level != null) { if (minecraft.level != null) {
this.inGameInputHandler().inputTick(); 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() { public ControlifyConfig config() {
@ -232,14 +250,24 @@ public class Controlify implements ControlifyApi {
} }
private void onControllerHotplugged(int jid) { 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()); LOGGER.info("Controller connected: " + controller.name());
config().loadOrCreateControllerData(currentController); config().loadOrCreateControllerData(currentController);
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false;
config().setDirty();
}
this.askToSwitchController(controller); this.askToSwitchController(controller);
checkCompoundJoysticks(); checkCompoundJoysticks();
config().saveIfDirty();
} }
private void onControllerDisconnect(int jid) { private void onControllerDisconnect(int jid) {
@ -304,7 +332,12 @@ public class Controlify implements ControlifyApi {
controller = Controller.DUMMY; controller = Controller.DUMMY;
if (this.currentController == controller) return; if (this.currentController == controller) return;
if (this.currentController != null)
this.currentController.close();
this.currentController = controller; this.currentController = controller;
this.currentController.open();
if (switchableController == controller) { if (switchableController == controller) {
switchableController = null; 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.Controller;
import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.api.event.ControlifyEvents; 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.compat.fapi.KeyBindingRegistryImplAccessor;
import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor; import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor;
import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor; import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor;
@ -38,6 +39,7 @@ public class ControllerBindings<T extends ControllerState> {
public final ControllerBinding<T> public final ControllerBinding<T>
WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT, WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT,
LOOK_UP, LOOK_DOWN, LOOK_LEFT, LOOK_RIGHT, LOOK_UP, LOOK_DOWN, LOOK_LEFT, LOOK_RIGHT,
GAMEPAD_GYRO_BUTTON,
JUMP, SNEAK, JUMP, SNEAK,
ATTACK, USE, ATTACK, USE,
SPRINT, SPRINT,
@ -110,6 +112,15 @@ public class ControllerBindings<T extends ControllerState> {
.defaultBind(GamepadBinds.RIGHT_STICK_RIGHT) .defaultBind(GamepadBinds.RIGHT_STICK_RIGHT)
.category(MOVEMENT_CATEGORY) .category(MOVEMENT_CATEGORY)
.build()); .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) register(JUMP = ControllerBindingBuilder.create(controller)
.identifier("controlify", "jump") .identifier("controlify", "jump")
.defaultBind(GamepadBinds.A_BUTTON) .defaultBind(GamepadBinds.A_BUTTON)

View File

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

View File

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

View File

@ -33,18 +33,18 @@ public class JoystickHatBind implements IBind<JoystickState> {
@Override @Override
public void draw(PoseStack matrices, int x, int centerY) { public void draw(PoseStack matrices, int x, int centerY) {
String type = joystick.type().identifier(); String type = joystick.type().identifier();
String button = joystick.mapping().button(hatIndex).identifier(); String hat = joystick.mapping().hats()[hatIndex].identifier();
String direction = "centered"; String direction = "centered";
if (hatState.isUp()) if (hatState.isUp())
direction = "up"; direction = "up";
else if (hatState.isDown()) else if (hatState.isDown())
direction = "down"; direction = "down";
else if (hatState.isLeft()) else if (hatState.isLeft())
direction = "strong"; direction = "left";
else if (hatState.isRight()) 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.setShaderTexture(0, texture);
RenderSystem.setShaderColor(1, 1, 1, 1); RenderSystem.setShaderColor(1, 1, 1, 1);

View File

@ -11,8 +11,8 @@ import org.quiltmc.json5.JsonReader;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
public record ControllerType(String friendlyName, String identifier) { public record ControllerType(String friendlyName, String identifier, boolean forceJoystick, boolean dontLoad) {
public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown"); public static final ControllerType UNKNOWN = new ControllerType("Unknown", "unknown", false, false);
private static Map<HIDIdentifier, ControllerType> typeMap = null; private static Map<HIDIdentifier, ControllerType> typeMap = null;
private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5"); 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()) { while (reader.hasNext()) {
String friendlyName = null; String friendlyName = null;
String identifier = null; String identifier = null;
boolean forceJoystick = false;
boolean dontLoad = false;
Set<HIDIdentifier> hids = new HashSet<>(); Set<HIDIdentifier> hids = new HashSet<>();
reader.beginObject(); reader.beginObject();
@ -77,6 +79,8 @@ public record ControllerType(String friendlyName, String identifier) {
} }
reader.endArray(); reader.endArray();
} }
case "force_joystick" -> forceJoystick = reader.nextBoolean();
case "dont_load" -> dontLoad = reader.nextBoolean();
default -> { default -> {
Controlify.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping..."); Controlify.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping...");
reader.skipValue(); reader.skipValue();
@ -90,7 +94,7 @@ public record ControllerType(String friendlyName, String identifier) {
continue; continue;
} }
var type = new ControllerType(friendlyName, identifier); var type = new ControllerType(friendlyName, identifier, forceJoystick, dontLoad);
for (var hid : hids) { for (var hid : hids) {
typeMap.put(hid, type); 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.buttonCount = joystickIds.stream().mapToInt(this::getButtonCountForJoystick).sum();
this.hatCount = joystickIds.stream().mapToInt(this::getHatCountForJoystick).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.config = new JoystickConfig(this);
this.defaultConfig = 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 record CompoundJoystickInfo(Collection<String> joystickUids, String friendlyName) {
public ControllerType type() { public ControllerType type() {
return new ControllerType(friendlyName, createUID(joystickUids)); return new ControllerType(friendlyName, createUID(joystickUids), true, false);
} }
public boolean canBeUsed() { public boolean canBeUsed() {

View File

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

View File

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

View File

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

View File

@ -1,123 +1,282 @@
package dev.isxander.controlify.controller.joystick.mapping; 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.Controlify;
import dev.isxander.controlify.bindings.JoystickAxisBind; import dev.isxander.controlify.bindings.JoystickAxisBind;
import dev.isxander.controlify.controller.ControllerType; 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.client.Minecraft;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.Vec2; 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.io.IOException;
import java.util.List; import java.util.*;
import java.util.Map;
public class RPJoystickMapping implements JoystickMapping { public class RPJoystickMapping implements JoystickMapping {
private static final Gson gson = new Gson();
private final Map<Integer, AxisMapping> axisMappings; private final AxisMapping[] axes;
private final Map<Integer, ButtonMapping> buttonMappings; private final ButtonMapping[] buttons;
private final Map<Integer, HatMapping> hatMappings; private final HatMapping[] hats;
public RPJoystickMapping(JsonObject object, ControllerType type) { public RPJoystickMapping(JsonReader reader, ControllerType type) throws IOException {
axisMappings = new HashMap<>(); AxisMapping[] axes = null;
object.getAsJsonArray("axes").forEach(element -> { ButtonMapping[] buttons = null;
var axis = element.getAsJsonObject(); HatMapping[] hats = null;
List<Integer> ids = axis.getAsJsonArray("ids").asList().stream().map(JsonElement::getAsInt).toList();
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 inpRange = null;
Vec2 outRange = null; Vec2 outRange = null;
if (axis.has("range")) { boolean deadzone = false;
var rangeElement = axis.get("range"); float restState = 0f;
if (rangeElement.isJsonArray()) { String identifier = null;
var rangeArray = rangeElement.getAsJsonArray(); List<String[]> axisNames = new ArrayList<>();
outRange = new Vec2(rangeArray.get(0).getAsFloat(), rangeArray.get(1).getAsFloat());
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); inpRange = new Vec2(-1, 1);
} else if (rangeElement.isJsonObject()) { reader.endArray();
var rangeObject = rangeElement.getAsJsonObject(); } else {
reader.beginObject();
while (reader.hasNext()) {
String rangeName = reader.nextName();
var inpRangeArray = rangeObject.getAsJsonArray("in"); switch (rangeName) {
inpRange = new Vec2(inpRangeArray.get(0).getAsFloat(), inpRangeArray.get(1).getAsFloat()); case "in" -> {
reader.beginArray();
var outRangeArray = rangeObject.getAsJsonArray("out"); inpRange = new Vec2((float) reader.nextDouble(), (float) reader.nextDouble());
outRange = new Vec2(outRangeArray.get(0).getAsFloat(), outRangeArray.get(1).getAsFloat()); 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(); reader.endObject();
var identifier = axis.get("identifier").getAsString(); }
}
var axisNames = axis.getAsJsonArray("axis_names").asList().stream() case "rest" -> {
.map(JsonElement::getAsJsonArray) restState = (float) reader.nextDouble();
.map(JsonArray::asList) }
.map(list -> list.stream().map(JsonElement::getAsString).toList()) case "deadzone" -> {
.toList(); 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) { 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<>(); return axes.toArray(new AxisMapping[0]);
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<>(); private ButtonMapping[] readButtons(JsonReader reader, ControllerType type) throws IOException {
object.getAsJsonArray("hats").forEach(element -> { List<ButtonMapping> buttons = new ArrayList<>();
var hat = element.getAsJsonObject();
hatMappings.put(hat.get("hat").getAsInt(), new HatMapping(hat.get("name").getAsString(), type.identifier())); 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 @Override
public Axis axis(int axis) { public Axis[] axes() {
if (!axisMappings.containsKey(axis)) return axes;
return UnmappedJoystickMapping.INSTANCE.axis(axis);
return axisMappings.get(axis);
} }
@Override @Override
public Button button(int button) { public Button[] buttons() {
if (!buttonMappings.containsKey(button)) return buttons;
return UnmappedJoystickMapping.INSTANCE.button(button);
return buttonMappings.get(button);
} }
@Override @Override
public Hat hat(int hat) { public Hat[] hats() {
if (!hatMappings.containsKey(hat)) return hats;
return UnmappedJoystickMapping.INSTANCE.hat(hat);
return hatMappings.get(hat);
} }
public static JoystickMapping fromType(ControllerType type) { public static JoystickMapping fromType(JoystickController<?> joystick) {
var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + type.identifier() + ".json")); var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + joystick.type().identifier() + ".json"));
if (resource.isEmpty()) { if (resource.isEmpty()) {
Controlify.LOGGER.warn("No joystick mapping found for controller: '" + type.identifier() + "'"); Controlify.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().identifier() + "'");
return UnmappedJoystickMapping.INSTANCE; return new UnmappedJoystickMapping(joystick.joystickId());
} }
try (var reader = resource.get().openAsReader()) { try (var reader = JsonReader.json5(resource.get().openAsReader())) {
return new RPJoystickMapping(gson.fromJson(reader, JsonObject.class), type); return new RPJoystickMapping(reader, joystick.type());
} catch (Exception e) { } catch (Exception e) {
Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + type.identifier() + "'", e); Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + joystick.type().identifier() + "'", e);
return UnmappedJoystickMapping.INSTANCE; 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 @Override
public float modifyAxis(float value) { public float getAxis(JoystickData data) {
if (inpRange() == null || outRange() == null) float rawAxis = data.axes()[id];
return value;
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 @Override
@ -132,23 +291,40 @@ public class RPJoystickMapping implements JoystickMapping {
@Override @Override
public String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction) { 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 @Override
public Component name() { public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".button." + identifier()); 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 @Override
public Component name() { public Component name() {
return Component.translatable("controlify.joystick_mapping." + typeId() + ".hat." + identifier()); 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; package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.bindings.JoystickAxisBind; 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 net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.Arrays;
public class UnmappedJoystickMapping implements JoystickMapping { public class UnmappedJoystickMapping implements JoystickMapping {
public static final UnmappedJoystickMapping INSTANCE = new UnmappedJoystickMapping(); public static final UnmappedJoystickMapping EMPTY = new UnmappedJoystickMapping(0, 0, 0);
@Override private final UnmappedAxis[] axes;
public Axis axis(int axis) { private final UnmappedButton[] buttons;
return new UnmappedAxis(axis); 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 @Override
public Button button(int button) { public Axis[] axes() {
return new UnmappedButton(button); return axes;
} }
@Override @Override
public Hat hat(int hat) { public Button[] buttons() {
return new UnmappedHat(hat); return buttons;
}
@Override
public Hat[] hats() {
return hats;
} }
private record UnmappedAxis(int axis) implements Axis { private record UnmappedAxis(int axis) implements Axis {
@Override
public float getAxis(JoystickData data) {
return data.axes()[axis];
}
@Override @Override
public String identifier() { public String identifier() {
@ -38,11 +76,6 @@ public class UnmappedJoystickMapping implements JoystickMapping {
return true; return true;
} }
@Override
public float modifyAxis(float value) {
return value;
}
@Override @Override
public boolean isAxisResting(float value) { public boolean isAxisResting(float value) {
return value == restingValue(); return value == restingValue();
@ -60,6 +93,11 @@ public class UnmappedJoystickMapping implements JoystickMapping {
} }
private record UnmappedButton(int button) implements Button { private record UnmappedButton(int button) implements Button {
@Override
public boolean isPressed(JoystickData data) {
return data.buttons()[button];
}
@Override @Override
public String identifier() { public String identifier() {
return "button-" + button; return "button-" + button;
@ -72,6 +110,11 @@ public class UnmappedJoystickMapping implements JoystickMapping {
} }
private record UnmappedHat(int hat) implements Hat { private record UnmappedHat(int hat) implements Hat {
@Override
public JoystickState.HatState getHatState(JoystickData data) {
return data.hats()[hat];
}
@Override @Override
public String identifier() { public String identifier() {
return "hat-" + hat; return "hat-" + hat;

View File

@ -1,12 +1,19 @@
package dev.isxander.controlify.debug; package dev.isxander.controlify.debug;
public class DebugProperties { import net.fabricmc.loader.api.FabricLoader;
// 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);
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))); return Boolean.parseBoolean(System.getProperty(name, Boolean.toString(def)));
} }
} }

View File

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

View File

@ -1,9 +1,12 @@
package dev.isxander.controlify.rumble; package dev.isxander.controlify.rumble;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Function; import java.util.function.Function;
public final class BasicRumbleEffect implements RumbleEffect { public final class BasicRumbleEffect implements RumbleEffect {
@ -11,6 +14,7 @@ public final class BasicRumbleEffect implements RumbleEffect {
private int tick = 0; private int tick = 0;
private boolean finished; private boolean finished;
private int priority = 0; private int priority = 0;
private BooleanSupplier earlyFinishCondition = () -> false;
public BasicRumbleEffect(RumbleState[] keyframes) { public BasicRumbleEffect(RumbleState[] keyframes) {
this.keyframes = keyframes; this.keyframes = keyframes;
@ -19,7 +23,7 @@ public final class BasicRumbleEffect implements RumbleEffect {
@Override @Override
public void tick() { public void tick() {
tick++; tick++;
if (tick >= keyframes.length) if (tick >= keyframes.length || earlyFinishCondition.getAsBoolean())
finished = true; finished = true;
} }
@ -55,6 +59,12 @@ public final class BasicRumbleEffect implements RumbleEffect {
return keyframes; return keyframes;
} }
public BasicRumbleEffect earlyFinish(BooleanSupplier condition) {
var current = earlyFinishCondition;
this.earlyFinishCondition = () -> current.getAsBoolean() || condition.getAsBoolean();
return this;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj == this) return true; if (obj == this) return true;
@ -76,6 +86,22 @@ public final class BasicRumbleEffect implements RumbleEffect {
"priority=" + this.priority() + ']'; "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. * 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); return new BasicRumbleEffect(states);
} }
public BasicRumbleEffect join(BasicRumbleEffect other) { public static BooleanSupplier finishOnScreenChange() {
return BasicRumbleEffect.join(this, other); Screen screen = Minecraft.getInstance().screen;
} return () -> screen != Minecraft.getInstance().screen;
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;
} }
} }

View File

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