1
0
forked from Clones/Controlify

gyro & look input modifier & deadzone bug

This commit is contained in:
isXander
2023-04-11 11:04:47 +01:00
parent d3fc0a946b
commit beece493c3
13 changed files with 416 additions and 104 deletions

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.api.event; package dev.isxander.controlify.api.event;
import dev.isxander.controlify.InputMode; import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.api.ingameinput.LookInputModifier;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry; import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry;
@ -44,6 +45,24 @@ public final class ControlifyEvents {
} }
}); });
public static final Event<LookInputModifier> LOOK_INPUT_MODIFIER = EventFactory.createArrayBacked(LookInputModifier.class, callbacks -> new LookInputModifier() {
@Override
public float modifyX(float x, Controller<?, ?> controller) {
for (LookInputModifier callback : callbacks) {
x = callback.modifyX(x, controller);
}
return x;
}
@Override
public float modifyY(float y, Controller<?, ?> controller) {
for (LookInputModifier callback : callbacks) {
y = callback.modifyY(y, controller);
}
return y;
}
});
@FunctionalInterface @FunctionalInterface
public interface InputModeChanged { public interface InputModeChanged {
void onInputModeChanged(InputMode mode); void onInputModeChanged(InputMode mode);
@ -63,4 +82,5 @@ public final class ControlifyEvents {
public interface VirtualMouseToggled { public interface VirtualMouseToggled {
void onVirtualMouseToggled(boolean enabled); void onVirtualMouseToggled(boolean enabled);
} }
} }

View File

@ -0,0 +1,21 @@
package dev.isxander.controlify.api.ingameinput;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.ingame.InGameInputHandler;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
public interface LookInputModifier {
float modifyX(float x, Controller<?, ?> controller);
float modifyY(float y, Controller<?, ?> controller);
static LookInputModifier functional(BiFunction<Float, Controller<?, ?>, Float> x, BiFunction<Float, Controller<?, ?>, Float> y) {
return new InGameInputHandler.FunctionalLookInputModifier(x, y);
}
static LookInputModifier zeroIf(BooleanSupplier condition) {
return functional((x, controller) -> condition.getAsBoolean() ? 0 : x, (y, controller) -> condition.getAsBoolean() ? 0 : y);
}
}

View File

@ -11,7 +11,6 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,6 +31,7 @@ public class ControlifyConfig {
private Map<String, CompoundJoystickInfo> compoundJoysticks = Map.of(); private Map<String, CompoundJoystickInfo> compoundJoysticks = Map.of();
private GlobalSettings globalSettings = new GlobalSettings(); private GlobalSettings globalSettings = new GlobalSettings();
private boolean firstLaunch; private boolean firstLaunch;
private boolean dirty; private boolean dirty;
public ControlifyConfig(Controlify controlify) { public ControlifyConfig(Controlify controlify) {
@ -101,14 +101,19 @@ public class ControlifyConfig {
private void applyConfig(JsonObject object) { private void applyConfig(JsonObject object) {
globalSettings = GSON.fromJson(object.getAsJsonObject("global"), GlobalSettings.class); globalSettings = GSON.fromJson(object.getAsJsonObject("global"), GlobalSettings.class);
if (globalSettings == null) globalSettings = new GlobalSettings(); if (globalSettings == null) {
globalSettings = new GlobalSettings();
setDirty();
}
JsonObject controllers = object.getAsJsonObject("controllers"); JsonObject controllers = object.getAsJsonObject("controllers");
if (controllers != null) { if (controllers != null) {
this.controllerData = controllers; this.controllerData = controllers;
for (var controller : Controller.CONTROLLERS.values()) { for (var controller : Controller.CONTROLLERS.values()) {
_loadOrCreateControllerData(controller); loadOrCreateControllerData(controller);
} }
} else {
setDirty();
} }
this.compoundJoysticks = object this.compoundJoysticks = object
@ -122,25 +127,18 @@ public class ControlifyConfig {
currentControllerUid = object.get("current_controller").getAsString(); currentControllerUid = object.get("current_controller").getAsString();
} else { } else {
currentControllerUid = controlify.currentController().uid(); currentControllerUid = controlify.currentController().uid();
setDirty();
} }
} }
public boolean loadOrCreateControllerData(Controller<?, ?> controller) { public void loadOrCreateControllerData(Controller<?, ?> controller) {
boolean result = _loadOrCreateControllerData(controller);
saveIfDirty();
return result;
}
private boolean _loadOrCreateControllerData(Controller<?, ?> controller) {
var uid = controller.uid(); var uid = controller.uid();
if (controllerData.has(uid)) { if (controllerData.has(uid)) {
Controlify.LOGGER.info("Loading controller data for " + uid); Controlify.LOGGER.info("Loading controller data for " + uid);
applyControllerConfig(controller, controllerData.getAsJsonObject(uid)); applyControllerConfig(controller, controllerData.getAsJsonObject(uid));
return true;
} else { } else {
Controlify.LOGGER.info("New controller found, creating controller data for " + uid); Controlify.LOGGER.info("New controller found, creating controller data for " + uid);
save(); setDirty();
return false;
} }
} }
@ -155,17 +153,17 @@ public class ControlifyConfig {
} }
} }
private void saveIfDirty() { public void setDirty() {
dirty = true;
}
public void saveIfDirty() {
if (dirty) { if (dirty) {
Controlify.LOGGER.info("Config is dirty. Saving..."); Controlify.LOGGER.info("Config is dirty. Saving...");
save(); save();
} }
} }
public Optional<JsonObject> getLoadedControllerConfig(String uid) {
return Optional.ofNullable(controllerData.getAsJsonObject(uid));
}
public Map<String, CompoundJoystickInfo> getCompoundJoysticks() { public Map<String, CompoundJoystickInfo> getCompoundJoysticks() {
return compoundJoysticks; return compoundJoysticks;
} }

View File

@ -13,6 +13,7 @@ import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.reacharound.ReachAroundMode;
import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.BasicRumbleEffect;
@ -236,6 +237,51 @@ public class YACLHelper {
} }
category.group(vibrationGroup.build()); category.group(vibrationGroup.build());
if (controller instanceof GamepadController gamepad && (gamepad.hasGyro() || true)) {
var gpCfg = gamepad.config();
var gpCfgDef = gamepad.defaultConfig();
Option<Float> gyroSensitivity;
List<Option<?>> gyroOptions = new ArrayList<>();
var gyroGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.gyro"))
.tooltip(Component.translatable("controlify.gui.group.gyro.tooltip"))
.option(gyroSensitivity = Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.gyro_look_sensitivity"))
.tooltip(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip"))
.binding(gpCfgDef.gyroLookSensitivity, () -> gpCfg.gyroLookSensitivity, v -> gpCfg.gyroLookSensitivity = v)
.controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter))
.listener((opt, sensitivity) -> gyroOptions.forEach(o -> {
o.setAvailable(sensitivity > 0);
o.requestSetDefault();
}))
.build())
.option(Util.make(() -> {
var opt = Option.createBuilder(boolean.class)
.name(Component.translatable("controlify.gui.gyro_requires_button"))
.tooltip(Component.translatable("controlify.gui.gyro_requires_button.tooltip"))
.binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v)
.controller(TickBoxController::new)
.available(gyroSensitivity.pendingValue() > 0)
.build();
gyroOptions.add(opt);
return opt;
}))
.option(Util.make(() -> {
var opt = Option.createBuilder(boolean.class)
.name(Component.translatable("controlify.gui.flick_stick"))
.tooltip(Component.translatable("controlify.gui.flick_stick.tooltip"))
.binding(gpCfgDef.flickStick, () -> gpCfg.flickStick, v -> gpCfg.flickStick = v)
.controller(TickBoxController::new)
.available(gyroSensitivity.pendingValue() > 0)
.build();
gyroOptions.add(opt);
return opt;
}));
category.group(gyroGroup.build());
}
var advancedGroup = OptionGroup.createBuilder() var advancedGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.advanced")) .name(Component.translatable("controlify.gui.group.advanced"))
.tooltip(Component.translatable("controlify.gui.group.advanced.tooltip")) .tooltip(Component.translatable("controlify.gui.group.advanced.tooltip"))
@ -274,11 +320,12 @@ public class YACLHelper {
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build()); .build());
} else if (controller instanceof SingleJoystickController joystick) { } else if (controller instanceof SingleJoystickController joystick) {
Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount()) JoystickMapping.Axis[] axes = joystick.mapping().axes();
.filter(i -> joystick.mapping().axis(i).requiresDeadzone()) Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
.filter(i -> axes[i].requiresDeadzone())
.boxed() .boxed()
.collect(Collectors.toMap( .collect(Collectors.toMap(
i -> joystick.mapping().axis(i).identifier(), i -> axes[i].identifier(),
i -> i, i -> i,
(x, y) -> x, (x, y) -> x,
LinkedHashMap::new LinkedHashMap::new
@ -288,9 +335,11 @@ public class YACLHelper {
var jsCfgDef = joystick.defaultConfig(); var jsCfgDef = joystick.defaultConfig();
for (int i : deadzoneAxes) { for (int i : deadzoneAxes) {
var axis = axes[i];
advancedGroup.option(Option.createBuilder(float.class) advancedGroup.option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", joystick.mapping().axis(i).name())) .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", joystick.mapping().axis(i).name())) .tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v)) .binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
@ -325,6 +374,7 @@ public class YACLHelper {
.join(BasicRumbleEffect.constant(0f, 1f, 5)) .join(BasicRumbleEffect.constant(0f, 1f, 5))
.repeat(10) .repeat(10)
) )
.earlyFinish(BasicRumbleEffect.finishOnScreenChange())
); );
}) })
.build()); .build());

View File

@ -24,8 +24,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
private final String uid; private final String uid;
private final String guid; private final String guid;
private final ControllerType type; private final ControllerType type;
private final long ptrJoystick;
private final RumbleManager rumbleManager;
private final ControllerBindings<S> bindings; private final ControllerBindings<S> bindings;
protected C config, defaultConfig; protected C config, defaultConfig;
@ -39,9 +37,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
this.joystickId = joystickId; this.joystickId = joystickId;
this.guid = GLFW.glfwGetJoystickGUID(joystickId); this.guid = GLFW.glfwGetJoystickGUID(joystickId);
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
this.rumbleManager = new RumbleManager(this);
if (hidInfo.path().isPresent()) { if (hidInfo.path().isPresent()) {
this.uid = hidInfo.createControllerUID().orElseThrow(); this.uid = hidInfo.createControllerUID().orElseThrow();
this.type = hidInfo.type(); this.type = hidInfo.type();
@ -115,6 +110,7 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
newConfig = gson.fromJson(json, new TypeToken<C>(getClass()){}.getType()); newConfig = gson.fromJson(json, new TypeToken<C>(getClass()){}.getType());
} catch (Exception e) { } catch (Exception e) {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead. Printing json: " + json.toString(), e); Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead. Printing json: " + json.toString(), e);
Controlify.instance().config().setDirty();
return; return;
} }
@ -123,46 +119,10 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
} else { } else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead."); Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig(); this.config = defaultConfig();
Controlify.instance().config().setDirty();
} }
} }
@Override
public boolean setRumble(float strongMagnitude, float weakMagnitude, RumbleSource source) {
if (!canRumble()) return false;
var strengthMod = config().getRumbleStrength(source);
if (source != RumbleSource.MASTER)
strengthMod *= config().getRumbleStrength(RumbleSource.MASTER);
strongMagnitude *= strengthMod;
weakMagnitude *= strengthMod;
// the duration doesn't matter because we are not updating the joystick state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_JoystickRumble(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 1)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;
}
@Override
public boolean canRumble() {
return SDL2NativesManager.isLoaded()
&& config().allowVibrations
&& ControlifyApi.get().currentInputMode() == InputMode.CONTROLLER;
}
@Override
public RumbleManager rumbleManager() {
return this.rumbleManager;
}
@Override
public void close() {
SDL.SDL_JoystickClose(ptrJoystick);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -2,6 +2,7 @@ package dev.isxander.controlify.controller;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.hid.ControllerHIDService;
@ -10,6 +11,9 @@ import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.rumble.RumbleCapable; import dev.isxander.controlify.rumble.RumbleCapable;
import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleManager;
import dev.isxander.controlify.rumble.RumbleSource; import dev.isxander.controlify.rumble.RumbleSource;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.util.HashMap; import java.util.HashMap;
@ -37,38 +41,49 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
void updateState(); void updateState();
void clearState(); void clearState();
default void open() {}
default void close() {}
RumbleManager rumbleManager(); RumbleManager rumbleManager();
default boolean canBeUsed() { default boolean canBeUsed() {
return true; return true;
} }
default void close() {
}
Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>(); Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { static Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
try {
Optional<String> uid = hidInfo.createControllerUID(); Optional<String> uid = hidInfo.createControllerUID();
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) { if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
return CONTROLLERS.get(uid.get()); return Optional.of(CONTROLLERS.get(uid.get()));
} }
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) { if (hidInfo.type().dontLoad()) {
Controlify.LOGGER.warn("Preventing load of controller #" + joystickId + " because its type prevents loading.");
return Optional.empty();
}
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) {
GamepadController controller = new GamepadController(joystickId, hidInfo); GamepadController controller = new GamepadController(joystickId, hidInfo);
CONTROLLERS.put(controller.uid(), controller); CONTROLLERS.put(controller.uid(), controller);
return controller; return Optional.of(controller);
} }
SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo); SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
CONTROLLERS.put(controller.uid(), controller); CONTROLLERS.put(controller.uid(), controller);
return controller; return Optional.of(controller);
} catch (Throwable e) {
CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickId);
CrashReportCategory category = crashReport.addCategory("Controller Info");
category.setDetail("Joystick ID", joystickId);
category.setDetail("Controller identification", hidInfo.type());
category.setDetail("HID path", hidInfo.path().orElse("N/A"));
throw new ReportedException(crashReport);
}
} }
static void remove(Controller<?, ?> controller) { static void remove(Controller<?, ?> controller) {
CONTROLLERS.remove(controller.uid(), controller); CONTROLLERS.remove(controller.uid(), controller);
controller.close();
} }
Controller<?, ?> DUMMY = new Controller<>() { Controller<?, ?> DUMMY = new Controller<>() {

View File

@ -5,10 +5,13 @@ import dev.isxander.controlify.controller.ControllerConfig;
public class GamepadConfig extends ControllerConfig { public class GamepadConfig extends ControllerConfig {
public float leftStickDeadzoneX = 0.2f; public float leftStickDeadzoneX = 0.2f;
public float leftStickDeadzoneY = 0.2f; public float leftStickDeadzoneY = 0.2f;
public float rightStickDeadzoneX = 0.2f; public float rightStickDeadzoneX = 0.2f;
public float rightStickDeadzoneY = 0.2f; public float rightStickDeadzoneY = 0.2f;
public float gyroLookSensitivity = 0f;
public boolean gyroRequiresButton = true;
public boolean flickStick = false;
public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT; public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT;
@Override @Override

View File

@ -1,7 +1,15 @@
package dev.isxander.controlify.controller.gamepad; package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.rumble.RumbleManager;
import dev.isxander.controlify.rumble.RumbleSource;
import org.libsdl.SDL;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWGamepadState; import org.lwjgl.glfw.GLFWGamepadState;
@ -9,6 +17,12 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
private GamepadState state = GamepadState.EMPTY; private GamepadState state = GamepadState.EMPTY;
private GamepadState prevState = GamepadState.EMPTY; private GamepadState prevState = GamepadState.EMPTY;
private long gamepadPtr;
private boolean rumbleSupported, triggerRumbleSupported;
private final RumbleManager rumbleManager;
private boolean hasGyro;
private GamepadState.GyroState absoluteGyro = GamepadState.GyroState.ORIGIN;
public GamepadController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { public GamepadController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo); super(joystickId, hidInfo);
if (!GLFW.glfwJoystickIsGamepad(joystickId)) if (!GLFW.glfwJoystickIsGamepad(joystickId))
@ -17,6 +31,8 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
if (!this.name.startsWith(type().friendlyName())) if (!this.name.startsWith(type().friendlyName()))
setName(GLFW.glfwGetGamepadName(joystickId)); setName(GLFW.glfwGetGamepadName(joystickId));
this.rumbleManager = new RumbleManager(this);
this.defaultConfig = new GamepadConfig(); this.defaultConfig = new GamepadConfig();
this.config = new GamepadConfig(); this.config = new GamepadConfig();
} }
@ -40,7 +56,26 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY) .leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY)
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY); .rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY);
GamepadState.ButtonState buttonState = GamepadState.ButtonState.fromController(this); GamepadState.ButtonState buttonState = GamepadState.ButtonState.fromController(this);
state = new GamepadState(axesState, rawAxesState, buttonState);
GamepadState.GyroState gyroDelta = null;
if (this.hasGyro) {
float[] gyro = new float[3];
SDL.SDL_GameControllerGetSensorData(gamepadPtr, SDL.SDL_SENSOR_GYRO, gyro, 3);
gyroDelta = new GamepadState.GyroState(gyro[0], gyro[1], gyro[2]);
if (DebugProperties.PRINT_GYRO) Controlify.LOGGER.info("Gyro delta: " + gyroDelta);
absoluteGyro = absoluteGyro.add(gyroDelta);
}
SDL.SDL_GameControllerUpdate();
state = new GamepadState(axesState, rawAxesState, buttonState, gyroDelta, absoluteGyro);
}
public GamepadState.GyroState absoluteGyroState() {
return absoluteGyro;
}
public boolean hasGyro() {
return hasGyro;
} }
@Override @Override
@ -49,7 +84,7 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
} }
public void consumeButtonState() { public void consumeButtonState() {
this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY); this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY, state().gyroDelta(), state().absoluteGyroPos());
} }
GLFWGamepadState getGamepadState() { GLFWGamepadState getGamepadState() {
@ -58,4 +93,60 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
return state; return state;
} }
@Override
public boolean setRumble(float strongMagnitude, float weakMagnitude, RumbleSource source) {
if (!canRumble()) return false;
var strengthMod = config().getRumbleStrength(source);
if (source != RumbleSource.MASTER)
strengthMod *= config().getRumbleStrength(RumbleSource.MASTER);
strongMagnitude *= strengthMod;
weakMagnitude *= strengthMod;
// the duration doesn't matter because we are not updating the gamecontroller state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_GameControllerRumble(gamepadPtr, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;
}
@Override
public boolean canRumble() {
return rumbleSupported
&& config().allowVibrations
&& ControlifyApi.get().currentInputMode() == InputMode.CONTROLLER;
}
@Override
public RumbleManager rumbleManager() {
return this.rumbleManager;
}
@Override
public void open() {
if (SDL2NativesManager.isLoaded()) {
this.gamepadPtr = SDL.SDL_GameControllerOpen(joystickId);
Controlify.LOGGER.info(SDL.SDL_GetError());
this.rumbleSupported = SDL.SDL_GameControllerHasRumble(gamepadPtr);
this.triggerRumbleSupported = SDL.SDL_GameControllerHasRumble(gamepadPtr);
if (this.hasGyro = SDL.SDL_GameControllerHasSensor(gamepadPtr, SDL.SDL_SENSOR_GYRO)) {
SDL.SDL_GameControllerSetSensorEnabled(gamepadPtr, SDL.SDL_SENSOR_GYRO, true);
}
} else {
this.gamepadPtr = 0;
this.rumbleSupported = false;
this.hasGyro = false;
}
}
@Override
public void close() {
SDL.SDL_GameControllerClose(gamepadPtr);
this.gamepadPtr = 0;
this.rumbleSupported = false;
this.hasGyro = false;
}
} }

View File

@ -2,25 +2,37 @@ package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.utils.ControllerUtils; import dev.isxander.controlify.utils.ControllerUtils;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
public final class GamepadState implements ControllerState { public final class GamepadState implements ControllerState {
public static final GamepadState EMPTY = new GamepadState(AxesState.EMPTY, AxesState.EMPTY, ButtonState.EMPTY); public static final GamepadState EMPTY = new GamepadState(AxesState.EMPTY, AxesState.EMPTY, ButtonState.EMPTY, GyroState.ORIGIN, GyroState.ORIGIN);
private final AxesState gamepadAxes; private final AxesState gamepadAxes;
private final AxesState rawGamepadAxes; private final AxesState rawGamepadAxes;
private final ButtonState gamepadButtons; private final ButtonState gamepadButtons;
private final GyroState absoluteGyroPos;
private final @Nullable GyroState gyroDelta;
private final List<Float> unnamedAxes; private final List<Float> unnamedAxes;
private final List<Float> unnamedRawAxes; private final List<Float> unnamedRawAxes;
private final List<Boolean> unnamedButtons; private final List<Boolean> unnamedButtons;
public GamepadState(AxesState gamepadAxes, AxesState rawGamepadAxes, ButtonState gamepadButtons) { public GamepadState(
AxesState gamepadAxes,
AxesState rawGamepadAxes,
ButtonState gamepadButtons,
@Nullable GamepadState.GyroState gyroDelta,
GyroState absoluteGyroPos
) {
this.gamepadAxes = gamepadAxes; this.gamepadAxes = gamepadAxes;
this.rawGamepadAxes = rawGamepadAxes; this.rawGamepadAxes = rawGamepadAxes;
this.gamepadButtons = gamepadButtons; this.gamepadButtons = gamepadButtons;
this.gyroDelta = gyroDelta;
this.absoluteGyroPos = absoluteGyroPos;
this.unnamedAxes = List.of( this.unnamedAxes = List.of(
gamepadAxes.leftStickX(), gamepadAxes.leftStickX(),
@ -90,6 +102,19 @@ public final class GamepadState implements ControllerState {
return gamepadButtons; return gamepadButtons;
} }
public GyroState gyroDelta() {
if (gyroDelta == null) return GyroState.ORIGIN;
return gyroDelta;
}
public GyroState absoluteGyroPos() {
return absoluteGyroPos;
}
public boolean supportsGyro() {
return gyroDelta != null;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj == this) return true; if (obj == this) return true;
@ -213,4 +238,12 @@ public final class GamepadState implements ControllerState {
return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick); return new ButtonState(a, b, x, y, leftBumper, rightBumper, back, start, guide, dpadUp, dpadDown, dpadLeft, dpadRight, leftStick, rightStick);
} }
} }
public record GyroState(float pitch, float yaw, float roll) {
public static GyroState ORIGIN = new GyroState(0, 0, 0);
public GyroState add(GyroState other) {
return new GyroState(pitch + other.pitch, yaw + other.yaw, roll + other.roll);
}
}
} }

View File

@ -2,12 +2,17 @@ package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping; import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
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.sdl2.SDL2NativesManager;
import org.lwjgl.glfw.GLFW; import dev.isxander.controlify.rumble.RumbleManager;
import dev.isxander.controlify.rumble.RumbleSource;
import org.libsdl.SDL;
import java.util.Objects; import java.util.Objects;
@ -15,6 +20,10 @@ public class SingleJoystickController extends AbstractController<JoystickState,
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY; private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final JoystickMapping mapping; private final JoystickMapping mapping;
private long ptrJoystick;
private RumbleManager rumbleManager;
private boolean rumbleSupported;
public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo); super(joystickId, hidInfo);
@ -75,4 +84,50 @@ public class SingleJoystickController extends AbstractController<JoystickState,
super.setConfig(gson, json); super.setConfig(gson, json);
this.config.setup(this); this.config.setup(this);
} }
@Override
public boolean setRumble(float strongMagnitude, float weakMagnitude, RumbleSource source) {
if (!canRumble()) return false;
var strengthMod = config().getRumbleStrength(source);
if (source != RumbleSource.MASTER)
strengthMod *= config().getRumbleStrength(RumbleSource.MASTER);
strongMagnitude *= strengthMod;
weakMagnitude *= strengthMod;
// the duration doesn't matter because we are not updating the joystick state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_JoystickRumbleTriggers(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 1)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;
}
@Override
public boolean canRumble() {
return rumbleSupported
&& config().allowVibrations
&& ControlifyApi.get().currentInputMode() == InputMode.CONTROLLER;
}
@Override
public RumbleManager rumbleManager() {
return this.rumbleManager;
}
@Override
public void open() {
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
this.rumbleSupported = SDL2NativesManager.isLoaded() && SDL.SDL_JoystickHasRumble(this.ptrJoystick);
this.rumbleManager = new RumbleManager(this);
}
@Override
public void close() {
SDL.SDL_JoystickClose(ptrJoystick);
this.rumbleSupported = false;
this.rumbleManager = null;
}
} }

View File

@ -117,7 +117,14 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
for (int i = 0; i < axes.size(); i++) { for (int i = 0; i < axes.size(); i++) {
var axis = axes.get(i); var axis = axes.get(i);
var minDeadzone = axis + 0.08f; var minDeadzone = axis + 0.08f;
controller.config().setDeadzone(i, (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95)); var deadzone = (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95);
if (Float.isNaN(deadzone)) {
Controlify.LOGGER.warn("Deadzone for axis {} is NaN, using default deadzone.", i);
deadzone = controller.defaultConfig().getDeadzone(i);
}
controller.config().setDeadzone(i, deadzone);
} }
} }

View File

@ -1,13 +1,16 @@
package dev.isxander.controlify.ingame; package dev.isxander.controlify.ingame;
import dev.isxander.controlify.InputMode; import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.api.ingameinput.LookInputModifier;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.player.KeyboardInput; import net.minecraft.client.player.KeyboardInput;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.ProjectileWeaponItem; import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
public class InGameInputHandler { public class InGameInputHandler {
private final Controller<?, ?> controller; private final Controller<?, ?> controller;
@ -63,23 +66,59 @@ public class InGameInputHandler {
protected void handlePlayerLookInput() { protected void handlePlayerLookInput() {
var player = this.minecraft.player; var player = this.minecraft.player;
var gamepad = controller instanceof GamepadController ? (GamepadController) controller : null;
// flick stick - turn 90 degrees immediately upon turning
// should be paired with gyro controls
if (gamepad != null && gamepad.config().flickStick) {
if (player != null) {
var turnAngle = 90 / 0.15f; // Entity#turn multiplies cursor delta by 0.15 to get rotation
player.turn(
(controller.bindings().LOOK_RIGHT.justPressed() ? turnAngle : 0)
- (controller.bindings().LOOK_LEFT.justPressed() ? turnAngle : 0),
(controller.bindings().LOOK_DOWN.justPressed() ? turnAngle : 0)
- (controller.bindings().LOOK_UP.justPressed() ? turnAngle : 0)
);
}
return;
}
// normal look input
var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state(); var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state();
var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state(); var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
impulseX *= Math.abs(impulseX);
impulseY *= Math.abs(impulseY);
if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive() && player != null) { if (controller.config().reduceAimingSensitivity && player != null && player.isUsingItem()) {
lookInputX = impulseX * Math.abs(impulseX) * controller.config().horizontalLookSensitivity;
lookInputY = impulseY * Math.abs(impulseY) * controller.config().verticalLookSensitivity;
if (controller.config().reduceAimingSensitivity && player.isUsingItem()) {
float aimMultiplier = switch (player.getUseItem().getUseAnimation()) { float aimMultiplier = switch (player.getUseItem().getUseAnimation()) {
case BOW, CROSSBOW, SPEAR -> 0.6f; case BOW, CROSSBOW, SPEAR -> 0.6f;
case SPYGLASS -> 0.2f; case SPYGLASS -> 0.2f;
default -> 1f; default -> 1f;
}; };
lookInputX *= aimMultiplier; impulseX *= aimMultiplier;
lookInputY *= aimMultiplier; impulseY *= aimMultiplier;
} }
// gyro input
if (gamepad != null
&& gamepad.hasGyro()
&& (!gamepad.config().gyroRequiresButton || gamepad.bindings().GAMEPAD_GYRO_BUTTON.held())
) {
var gyroDelta = gamepad.state().gyroDelta();
impulseX += (gyroDelta.yaw() + gyroDelta.pitch()) * gamepad.config().gyroLookSensitivity;
impulseY += gyroDelta.roll() * gamepad.config().gyroLookSensitivity;
}
LookInputModifier lookInputModifier = ControlifyEvents.LOOK_INPUT_MODIFIER.invoker();
impulseX = lookInputModifier.modifyX(impulseX, controller);
impulseY = lookInputModifier.modifyY(impulseY, controller);
if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive() && player != null) {
lookInputX = impulseX * controller.config().horizontalLookSensitivity;
lookInputY = impulseY * controller.config().verticalLookSensitivity;
} else { } else {
lookInputX = lookInputY = 0; lookInputX = lookInputY = 0;
} }
@ -90,4 +129,16 @@ public class InGameInputHandler {
minecraft.player.turn(lookInputX * 65f * deltaTime, lookInputY * 65f * deltaTime); minecraft.player.turn(lookInputX * 65f * deltaTime, lookInputY * 65f * deltaTime);
} }
} }
public record FunctionalLookInputModifier(BiFunction<Float, Controller<?, ?>, Float> x, BiFunction<Float, Controller<?, ?>, Float> y) implements LookInputModifier {
@Override
public float modifyX(float x, Controller<?, ?> controller) {
return this.x.apply(x, controller);
}
@Override
public float modifyY(float y, Controller<?, ?> controller) {
return this.y.apply(y, controller);
}
}
} }

View File

@ -49,6 +49,14 @@
"controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.", "controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.",
"controlify.gui.group.vibration": "Vibration", "controlify.gui.group.vibration": "Vibration",
"controlify.gui.group.vibration.tooltip": "Adjust how your controller vibrates.", "controlify.gui.group.vibration.tooltip": "Adjust how your controller vibrates.",
"controlify.gui.group.gyro": "Gyro",
"controlify.gui.group.gyro.tooltip": "Adjust how Controlify treats your controller's built in gyroscope.\nA gyroscope determines how the controller is rotated.",
"controlify.gui.gyro_look_sensitivity": "Look Sensitivity",
"controlify.gui.gyro_look_sensitivity.tooltip": "How much the camera moves based on gyroscope rotation.",
"controlify.gui.gyro_requires_button": "Require Button",
"controlify.gui.gyro_requires_button.tooltip": "If the gyroscope should only be used when the gyro bind is pressed down. (scroll down to controls).",
"controlify.gui.flick_stick": "Flick Stick",
"controlify.gui.flick_stick.tooltip": "Changes the behaviour of the look up/down/left/right binds to rotate the look direction 90 degrees in the respected direction upon press. This should be combined with gyro look to get the most accurate and fast aiming.",
"controlify.gui.group.advanced": "Advanced", "controlify.gui.group.advanced": "Advanced",
"controlify.gui.group.advanced.tooltip": "Settings you probably shouldn't touch!.", "controlify.gui.group.advanced.tooltip": "Settings you probably shouldn't touch!.",
"controlify.gui.screen_repeat_navi_delay": "Screen Repeat Navigation Delay", "controlify.gui.screen_repeat_navi_delay": "Screen Repeat Navigation Delay",