forked from Clones/Controlify
gyro & look input modifier & deadzone bug
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package dev.isxander.controlify.api.event;
|
||||
|
||||
import dev.isxander.controlify.InputMode;
|
||||
import dev.isxander.controlify.api.ingameinput.LookInputModifier;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
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
|
||||
public interface InputModeChanged {
|
||||
void onInputModeChanged(InputMode mode);
|
||||
@ -63,4 +82,5 @@ public final class ControlifyEvents {
|
||||
public interface VirtualMouseToggled {
|
||||
void onVirtualMouseToggled(boolean enabled);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -32,6 +31,7 @@ public class ControlifyConfig {
|
||||
private Map<String, CompoundJoystickInfo> compoundJoysticks = Map.of();
|
||||
private GlobalSettings globalSettings = new GlobalSettings();
|
||||
private boolean firstLaunch;
|
||||
|
||||
private boolean dirty;
|
||||
|
||||
public ControlifyConfig(Controlify controlify) {
|
||||
@ -101,14 +101,19 @@ public class ControlifyConfig {
|
||||
|
||||
private void applyConfig(JsonObject object) {
|
||||
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");
|
||||
if (controllers != null) {
|
||||
this.controllerData = controllers;
|
||||
for (var controller : Controller.CONTROLLERS.values()) {
|
||||
_loadOrCreateControllerData(controller);
|
||||
loadOrCreateControllerData(controller);
|
||||
}
|
||||
} else {
|
||||
setDirty();
|
||||
}
|
||||
|
||||
this.compoundJoysticks = object
|
||||
@ -122,25 +127,18 @@ public class ControlifyConfig {
|
||||
currentControllerUid = object.get("current_controller").getAsString();
|
||||
} else {
|
||||
currentControllerUid = controlify.currentController().uid();
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean loadOrCreateControllerData(Controller<?, ?> controller) {
|
||||
boolean result = _loadOrCreateControllerData(controller);
|
||||
saveIfDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean _loadOrCreateControllerData(Controller<?, ?> controller) {
|
||||
public void loadOrCreateControllerData(Controller<?, ?> controller) {
|
||||
var uid = controller.uid();
|
||||
if (controllerData.has(uid)) {
|
||||
Controlify.LOGGER.info("Loading controller data for " + uid);
|
||||
applyControllerConfig(controller, controllerData.getAsJsonObject(uid));
|
||||
return true;
|
||||
} else {
|
||||
Controlify.LOGGER.info("New controller found, creating controller data for " + uid);
|
||||
save();
|
||||
return false;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,17 +153,17 @@ public class ControlifyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveIfDirty() {
|
||||
public void setDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
public void saveIfDirty() {
|
||||
if (dirty) {
|
||||
Controlify.LOGGER.info("Config is dirty. Saving...");
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<JsonObject> getLoadedControllerConfig(String uid) {
|
||||
return Optional.ofNullable(controllerData.getAsJsonObject(uid));
|
||||
}
|
||||
|
||||
public Map<String, CompoundJoystickInfo> getCompoundJoysticks() {
|
||||
return compoundJoysticks;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
|
||||
import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||
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.reacharound.ReachAroundMode;
|
||||
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
||||
@ -236,6 +237,51 @@ public class YACLHelper {
|
||||
}
|
||||
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()
|
||||
.name(Component.translatable("controlify.gui.group.advanced"))
|
||||
.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))))
|
||||
.build());
|
||||
} else if (controller instanceof SingleJoystickController joystick) {
|
||||
Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount())
|
||||
.filter(i -> joystick.mapping().axis(i).requiresDeadzone())
|
||||
JoystickMapping.Axis[] axes = joystick.mapping().axes();
|
||||
Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
|
||||
.filter(i -> axes[i].requiresDeadzone())
|
||||
.boxed()
|
||||
.collect(Collectors.toMap(
|
||||
i -> joystick.mapping().axis(i).identifier(),
|
||||
i -> axes[i].identifier(),
|
||||
i -> i,
|
||||
(x, y) -> x,
|
||||
LinkedHashMap::new
|
||||
@ -288,9 +335,11 @@ public class YACLHelper {
|
||||
var jsCfgDef = joystick.defaultConfig();
|
||||
|
||||
for (int i : deadzoneAxes) {
|
||||
var axis = axes[i];
|
||||
|
||||
advancedGroup.option(Option.createBuilder(float.class)
|
||||
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", joystick.mapping().axis(i).name()))
|
||||
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", joystick.mapping().axis(i).name()))
|
||||
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
|
||||
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
|
||||
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
||||
.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))))
|
||||
@ -325,6 +374,7 @@ public class YACLHelper {
|
||||
.join(BasicRumbleEffect.constant(0f, 1f, 5))
|
||||
.repeat(10)
|
||||
)
|
||||
.earlyFinish(BasicRumbleEffect.finishOnScreenChange())
|
||||
);
|
||||
})
|
||||
.build());
|
||||
|
@ -24,8 +24,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
||||
private final String uid;
|
||||
private final String guid;
|
||||
private final ControllerType type;
|
||||
private final long ptrJoystick;
|
||||
private final RumbleManager rumbleManager;
|
||||
|
||||
private final ControllerBindings<S> bindings;
|
||||
protected C config, defaultConfig;
|
||||
@ -39,9 +37,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
||||
this.joystickId = joystickId;
|
||||
this.guid = GLFW.glfwGetJoystickGUID(joystickId);
|
||||
|
||||
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
|
||||
this.rumbleManager = new RumbleManager(this);
|
||||
|
||||
if (hidInfo.path().isPresent()) {
|
||||
this.uid = hidInfo.createControllerUID().orElseThrow();
|
||||
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());
|
||||
} catch (Exception 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;
|
||||
}
|
||||
|
||||
@ -123,46 +119,10 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
||||
} else {
|
||||
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
|
||||
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
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -2,6 +2,7 @@ package dev.isxander.controlify.controller;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
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.RumbleManager;
|
||||
import dev.isxander.controlify.rumble.RumbleSource;
|
||||
import net.minecraft.CrashReport;
|
||||
import net.minecraft.CrashReportCategory;
|
||||
import net.minecraft.ReportedException;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -37,38 +41,49 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
void updateState();
|
||||
void clearState();
|
||||
|
||||
default void open() {}
|
||||
default void close() {}
|
||||
RumbleManager rumbleManager();
|
||||
|
||||
default boolean canBeUsed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void close() {
|
||||
|
||||
}
|
||||
|
||||
Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
|
||||
|
||||
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
Optional<String> uid = hidInfo.createControllerUID();
|
||||
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
||||
return CONTROLLERS.get(uid.get());
|
||||
}
|
||||
static Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
try {
|
||||
Optional<String> uid = hidInfo.createControllerUID();
|
||||
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
||||
return Optional.of(CONTROLLERS.get(uid.get()));
|
||||
}
|
||||
|
||||
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) {
|
||||
GamepadController controller = new GamepadController(joystickId, hidInfo);
|
||||
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);
|
||||
CONTROLLERS.put(controller.uid(), controller);
|
||||
return Optional.of(controller);
|
||||
}
|
||||
|
||||
SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
|
||||
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);
|
||||
}
|
||||
|
||||
SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
|
||||
CONTROLLERS.put(controller.uid(), controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
static void remove(Controller<?, ?> controller) {
|
||||
CONTROLLERS.remove(controller.uid(), controller);
|
||||
controller.close();
|
||||
}
|
||||
|
||||
Controller<?, ?> DUMMY = new Controller<>() {
|
||||
|
@ -5,10 +5,13 @@ import dev.isxander.controlify.controller.ControllerConfig;
|
||||
public class GamepadConfig extends ControllerConfig {
|
||||
public float leftStickDeadzoneX = 0.2f;
|
||||
public float leftStickDeadzoneY = 0.2f;
|
||||
|
||||
public float rightStickDeadzoneX = 0.2f;
|
||||
public float rightStickDeadzoneY = 0.2f;
|
||||
|
||||
public float gyroLookSensitivity = 0f;
|
||||
public boolean gyroRequiresButton = true;
|
||||
public boolean flickStick = false;
|
||||
|
||||
public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT;
|
||||
|
||||
@Override
|
||||
|
@ -1,7 +1,15 @@
|
||||
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.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.GLFWGamepadState;
|
||||
|
||||
@ -9,6 +17,12 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
private GamepadState state = 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) {
|
||||
super(joystickId, hidInfo);
|
||||
if (!GLFW.glfwJoystickIsGamepad(joystickId))
|
||||
@ -17,6 +31,8 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
if (!this.name.startsWith(type().friendlyName()))
|
||||
setName(GLFW.glfwGetGamepadName(joystickId));
|
||||
|
||||
this.rumbleManager = new RumbleManager(this);
|
||||
|
||||
this.defaultConfig = new GamepadConfig();
|
||||
this.config = new GamepadConfig();
|
||||
}
|
||||
@ -40,7 +56,26 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY)
|
||||
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY);
|
||||
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
|
||||
@ -49,7 +84,7 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -58,4 +93,60 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,25 +2,37 @@ package dev.isxander.controlify.controller.gamepad;
|
||||
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import dev.isxander.controlify.utils.ControllerUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
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 rawGamepadAxes;
|
||||
private final ButtonState gamepadButtons;
|
||||
|
||||
private final GyroState absoluteGyroPos;
|
||||
private final @Nullable GyroState gyroDelta;
|
||||
|
||||
private final List<Float> unnamedAxes;
|
||||
private final List<Float> unnamedRawAxes;
|
||||
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.rawGamepadAxes = rawGamepadAxes;
|
||||
this.gamepadButtons = gamepadButtons;
|
||||
this.gyroDelta = gyroDelta;
|
||||
this.absoluteGyroPos = absoluteGyroPos;
|
||||
|
||||
this.unnamedAxes = List.of(
|
||||
gamepadAxes.leftStickX(),
|
||||
@ -90,6 +102,19 @@ public final class GamepadState implements ControllerState {
|
||||
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
|
||||
public boolean equals(Object obj) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,17 @@ package dev.isxander.controlify.controller.joystick;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
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.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
import dev.isxander.controlify.rumble.RumbleSource;
|
||||
import org.libsdl.SDL;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@ -15,6 +20,10 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
|
||||
private final JoystickMapping mapping;
|
||||
|
||||
private long ptrJoystick;
|
||||
private RumbleManager rumbleManager;
|
||||
private boolean rumbleSupported;
|
||||
|
||||
public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
super(joystickId, hidInfo);
|
||||
|
||||
@ -75,4 +84,50 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
super.setConfig(gson, json);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,14 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
for (int i = 0; i < axes.size(); i++) {
|
||||
var axis = axes.get(i);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
package dev.isxander.controlify.ingame;
|
||||
|
||||
import dev.isxander.controlify.InputMode;
|
||||
import dev.isxander.controlify.api.ingameinput.LookInputModifier;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.api.event.ControlifyEvents;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.KeyboardInput;
|
||||
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 {
|
||||
private final Controller<?, ?> controller;
|
||||
@ -63,23 +66,59 @@ public class InGameInputHandler {
|
||||
|
||||
protected void handlePlayerLookInput() {
|
||||
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 impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
|
||||
impulseX *= Math.abs(impulseX);
|
||||
impulseY *= Math.abs(impulseY);
|
||||
|
||||
if (controller.config().reduceAimingSensitivity && player != null && player.isUsingItem()) {
|
||||
float aimMultiplier = switch (player.getUseItem().getUseAnimation()) {
|
||||
case BOW, CROSSBOW, SPEAR -> 0.6f;
|
||||
case SPYGLASS -> 0.2f;
|
||||
default -> 1f;
|
||||
};
|
||||
impulseX *= 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 * 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()) {
|
||||
case BOW, CROSSBOW, SPEAR -> 0.6f;
|
||||
case SPYGLASS -> 0.2f;
|
||||
default -> 1f;
|
||||
};
|
||||
lookInputX *= aimMultiplier;
|
||||
lookInputY *= aimMultiplier;
|
||||
}
|
||||
lookInputX = impulseX * controller.config().horizontalLookSensitivity;
|
||||
lookInputY = impulseY * controller.config().verticalLookSensitivity;
|
||||
} else {
|
||||
lookInputX = lookInputY = 0;
|
||||
}
|
||||
@ -90,4 +129,16 @@ public class InGameInputHandler {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,14 @@
|
||||
"controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.",
|
||||
"controlify.gui.group.vibration": "Vibration",
|
||||
"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.tooltip": "Settings you probably shouldn't touch!.",
|
||||
"controlify.gui.screen_repeat_navi_delay": "Screen Repeat Navigation Delay",
|
||||
|
Reference in New Issue
Block a user