forked from Clones/Controlify
🎮📳 Controller Vibration! (#38)
This commit is contained in:
@ -18,6 +18,7 @@ group = "dev.isxander"
|
||||
version = "1.1.0+1.19.4"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven("https://maven.terraformersmc.com")
|
||||
maven("https://maven.isxander.dev/releases")
|
||||
@ -30,6 +31,7 @@ repositories {
|
||||
}
|
||||
}
|
||||
maven("https://jitpack.io")
|
||||
mavenLocal()
|
||||
maven("https://maven.flashyreese.me/snapshots")
|
||||
}
|
||||
|
||||
@ -88,6 +90,10 @@ dependencies {
|
||||
implementation(libs.hid4java)
|
||||
include(libs.hid4java)
|
||||
|
||||
// controller rumble
|
||||
implementation(libs.sdl2.jni)
|
||||
include(libs.sdl2.jni)
|
||||
|
||||
// used to parse hiddb.json5
|
||||
implementation(libs.quilt.json5)
|
||||
include(libs.quilt.json5)
|
||||
|
@ -10,9 +10,9 @@
|
||||
| **Controller button rendering** | Powered by resource pack controller detection | Texture atlas for hardcoded identifiers |
|
||||
| **In-game look sensitivity & behaviour** | Emulated Bedrock Edition with good defaults and snappy behaviour | testing required - reported bad defaults |
|
||||
| **Container interaction** | Controlled cursor snaps to container slots (with API) with power of left click, right click and shift click | No slot snapping, testing required |
|
||||
| **Touchscreen support** | ⛔ | ✅ |
|
||||
| **Touchscreen support** | ⛔ | ✅ No multi-touch support |
|
||||
| **Joystick support** | ✅ Multiple joysticks can be combined together (no UI yet) | ✅ Multiple joysticks can be combined together |
|
||||
| **Joystick mapping** | Powered by resource packs, unlimited amount of buttons available | All combined joysticks limited to gamepad input. Powered by SDL mappings |
|
||||
| **Configurability** | Each controller has individual settings with the ability to map every single controller action. No hardcoding! | needs testing |
|
||||
| **Mod keybindings** | All keybinds work out of box after mapping buttons | All keybinds work out of box after mapping buttons |
|
||||
| | | |
|
||||
| **Controller rumble** | ✅ | ⛔ |
|
||||
|
@ -19,6 +19,7 @@ quilt_json5 = "1.0.3"
|
||||
sodium = "mc1.19.4-0.4.10"
|
||||
iris = "1.5.2+1.19.4"
|
||||
immediately_fast = "1.1.10+1.19.4"
|
||||
sdl2_jni = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
|
||||
@ -33,6 +34,7 @@ quilt_json5 = { module = "org.quiltmc:quilt-json5", version.ref = "quilt_json5"
|
||||
sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" }
|
||||
iris = { module = "maven.modrinth:iris", version.ref = "iris" }
|
||||
immediately_fast = { module = "maven.modrinth:immediatelyfast", version.ref = "immediately_fast" }
|
||||
sdl2_jni = { module = "dev.isxander:sdl2-jni", version.ref = "sdl2_jni" }
|
||||
|
||||
test_fabric_loader = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric_loader" }
|
||||
|
||||
|
@ -7,7 +7,9 @@ import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||
import dev.isxander.controlify.gui.screen.VibrationOnboardingScreen;
|
||||
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
|
||||
import dev.isxander.controlify.config.ControlifyConfig;
|
||||
import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
||||
@ -53,17 +55,34 @@ public class Controlify implements ControlifyApi {
|
||||
private double askSwitchTime = 0;
|
||||
private ToastUtils.ControlifyToast askSwitchToast = null;
|
||||
|
||||
public void initializeControllers() {
|
||||
public void initializeControlify() {
|
||||
LOGGER.info("Initializing Controlify...");
|
||||
|
||||
config().load();
|
||||
|
||||
if (!config().globalSettings().vibrationOnboarded) {
|
||||
minecraft.setScreen(new VibrationOnboardingScreen(
|
||||
minecraft.screen,
|
||||
answer -> this.initializeControllers()
|
||||
));
|
||||
} else {
|
||||
this.initializeControllers();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeControllers() {
|
||||
LOGGER.info("Discovering and initializing controllers...");
|
||||
|
||||
config().load();
|
||||
|
||||
controllerHIDService = new ControllerHIDService();
|
||||
controllerHIDService.start();
|
||||
if (config().globalSettings().loadVibrationNatives)
|
||||
SDL2NativesManager.initialise();
|
||||
|
||||
boolean dirtyControllerConfig = false;
|
||||
// find already connected controllers
|
||||
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
|
||||
if (GLFW.glfwJoystickPresent(jid)) {
|
||||
try {
|
||||
var controller = Controller.createOrGet(jid, controllerHIDService.fetchType());
|
||||
LOGGER.info("Controller found: " + controller.name());
|
||||
|
||||
@ -71,7 +90,19 @@ public class Controlify implements ControlifyApi {
|
||||
|
||||
if (config().currentControllerUid().equals(controller.uid()))
|
||||
setCurrentController(controller);
|
||||
|
||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
||||
controller.config().allowVibrations = false;
|
||||
dirtyControllerConfig = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to initialize controller with jid " + jid, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dirtyControllerConfig) {
|
||||
config().save();
|
||||
}
|
||||
|
||||
checkCompoundJoysticks();
|
||||
@ -108,12 +139,15 @@ public class Controlify implements ControlifyApi {
|
||||
});
|
||||
}
|
||||
|
||||
public void initializeControlify() {
|
||||
public void preInitialiseControlify() {
|
||||
LOGGER.info("Pre-initializing Controlify...");
|
||||
|
||||
this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller
|
||||
this.virtualMouseHandler = new VirtualMouseHandler();
|
||||
|
||||
controllerHIDService = new ControllerHIDService();
|
||||
controllerHIDService.start();
|
||||
|
||||
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
|
||||
try {
|
||||
entrypoint.onControlifyPreInit(this);
|
||||
@ -139,8 +173,11 @@ public class Controlify implements ControlifyApi {
|
||||
for (var controller : Controller.CONTROLLERS.values()) {
|
||||
if (!outOfFocus)
|
||||
controller.updateState();
|
||||
else
|
||||
else {
|
||||
controller.clearState();
|
||||
controller.rumbleManager().stopCurrentEffect();
|
||||
}
|
||||
controller.rumbleManager().tick();
|
||||
}
|
||||
|
||||
ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state();
|
||||
|
@ -5,11 +5,11 @@ package dev.isxander.controlify.api.buttonguide;
|
||||
*/
|
||||
public enum ButtonRenderPosition {
|
||||
/**
|
||||
* Renders outside the button the left.
|
||||
* Renders outside the button the left edge of the screen.
|
||||
*/
|
||||
LEFT,
|
||||
/**
|
||||
* Renders outside the button the right.
|
||||
* Renders outside the button the right edge of the screen.
|
||||
*/
|
||||
RIGHT,
|
||||
/**
|
||||
|
@ -40,9 +40,9 @@ public class JoystickHatBind implements IBind<JoystickState> {
|
||||
else if (hatState.isDown())
|
||||
direction = "down";
|
||||
else if (hatState.isLeft())
|
||||
direction = "left";
|
||||
direction = "strong";
|
||||
else if (hatState.isRight())
|
||||
direction = "right";
|
||||
direction = "weak";
|
||||
|
||||
var texture = new ResourceLocation("controlify", "textures/gui/joystick/" + type + "/hat" + button + "_" + direction + ".png");
|
||||
|
||||
|
@ -15,5 +15,7 @@ public class GlobalSettings {
|
||||
|
||||
public boolean keyboardMovement = false;
|
||||
public boolean outOfFocusInput = false;
|
||||
public boolean loadVibrationNatives = false;
|
||||
public boolean vibrationOnboarded = false;
|
||||
public ReachAroundMode reachAround = ReachAroundMode.OFF;
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.JoystickState;
|
||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||
import dev.isxander.controlify.reacharound.ReachAroundMode;
|
||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||
import dev.isxander.controlify.rumble.RumbleState;
|
||||
import dev.isxander.yacl.api.*;
|
||||
import dev.isxander.yacl.gui.controllers.ActionController;
|
||||
import dev.isxander.yacl.gui.controllers.BooleanController;
|
||||
@ -46,6 +48,8 @@ public class YACLHelper {
|
||||
.title(Component.literal("Controlify"))
|
||||
.save(() -> controlify.config().save());
|
||||
|
||||
Option<Boolean> globalVibrationOption;
|
||||
|
||||
var globalSettings = Controlify.instance().config().globalSettings();
|
||||
var globalCategory = ConfigCategory.createBuilder()
|
||||
.name(Component.translatable("controlify.gui.category.global"))
|
||||
@ -55,6 +59,14 @@ public class YACLHelper {
|
||||
.binding(Controlify.instance().currentController(), () -> Controlify.instance().currentController(), v -> Controlify.instance().setCurrentController(v))
|
||||
.controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), Controller.CONTROLLERS.values().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name())))
|
||||
.build())
|
||||
.option(globalVibrationOption = Option.createBuilder(boolean.class)
|
||||
.name(Component.translatable("controlify.gui.load_vibration_natives"))
|
||||
.tooltip(Component.translatable("controlify.gui.load_vibration_natives.tooltip"))
|
||||
.tooltip(Component.translatable("controlify.gui.load_vibration_natives.tooltip.warning").withStyle(ChatFormatting.RED))
|
||||
.binding(GlobalSettings.DEFAULT.loadVibrationNatives, () -> globalSettings.loadVibrationNatives, v -> globalSettings.loadVibrationNatives = v)
|
||||
.controller(opt -> new BooleanController(opt, BooleanController.YES_NO_FORMATTER, false))
|
||||
.flag(OptionFlag.GAME_RESTART)
|
||||
.build())
|
||||
.option(Option.createBuilder(ReachAroundMode.class)
|
||||
.name(Component.translatable("controlify.gui.reach_around"))
|
||||
.tooltip(Component.translatable("controlify.gui.reach_around.tooltip"))
|
||||
@ -84,13 +96,13 @@ public class YACLHelper {
|
||||
yacl.category(globalCategory.build());
|
||||
|
||||
for (var controller : Controller.CONTROLLERS.values()) {
|
||||
yacl.category(createControllerCategory(controller));
|
||||
yacl.category(createControllerCategory(controller, globalVibrationOption));
|
||||
}
|
||||
|
||||
return yacl.build().generateScreen(parent);
|
||||
}
|
||||
|
||||
private static ConfigCategory createControllerCategory(Controller<?, ?> controller) {
|
||||
private static ConfigCategory createControllerCategory(Controller<?, ?> controller, Option<Boolean> globalVibrationOption) {
|
||||
if (!controller.canBeUsed()) {
|
||||
return PlaceholderCategory.createBuilder()
|
||||
.name(Component.literal(controller.name()))
|
||||
@ -141,6 +153,13 @@ public class YACLHelper {
|
||||
.binding(def.autoJump, () -> config.autoJump, v -> config.autoJump = v)
|
||||
.controller(BooleanController::new)
|
||||
.build())
|
||||
.option(Option.createBuilder(boolean.class)
|
||||
.name(Component.translatable("controlify.gui.allow_vibrations"))
|
||||
.tooltip(Component.translatable("controlify.gui.allow_vibrations.tooltip"))
|
||||
.binding(globalVibrationOption.pendingValue(), () -> config.allowVibrations && globalVibrationOption.pendingValue(), v -> config.allowVibrations = v)
|
||||
.available(globalVibrationOption.pendingValue())
|
||||
.controller(TickBoxController::new)
|
||||
.build())
|
||||
.option(Option.createBuilder(boolean.class)
|
||||
.name(Component.translatable("controlify.gui.show_ingame_guide"))
|
||||
.tooltip(Component.translatable("controlify.gui.show_ingame_guide.tooltip"))
|
||||
@ -166,9 +185,9 @@ public class YACLHelper {
|
||||
.controller(opt -> new FloatSliderController(opt, 0f, 0.8f, 0.1f, percentFormatter))
|
||||
.build())
|
||||
.option(Option.createBuilder(boolean.class)
|
||||
.name(Component.translatable("controlify.gui.reduce_bow_sensitivity"))
|
||||
.tooltip(Component.translatable("controlify.gui.reduce_bow_sensitivity.tooltip"))
|
||||
.binding(def.reduceBowSensitivity, () -> config.reduceBowSensitivity, v -> config.reduceBowSensitivity = v)
|
||||
.name(Component.translatable("controlify.gui.reduce_aiming_sensitivity"))
|
||||
.tooltip(Component.translatable("controlify.gui.reduce_aiming_sensitivity.tooltip"))
|
||||
.binding(def.reduceAimingSensitivity, () -> config.reduceAimingSensitivity, v -> config.reduceAimingSensitivity = v)
|
||||
.controller(TickBoxController::new)
|
||||
.build());
|
||||
|
||||
@ -268,7 +287,21 @@ public class YACLHelper {
|
||||
.tooltip(Component.translatable("controlify.gui.button_activation_threshold.tooltip"))
|
||||
.binding(def.buttonActivationThreshold, () -> config.buttonActivationThreshold, v -> config.buttonActivationThreshold = v)
|
||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
||||
.build());
|
||||
.build())
|
||||
.option(ButtonOption.createBuilder()
|
||||
.name(Component.translatable("controlify.gui.test_vibration"))
|
||||
.tooltip(Component.translatable("controlify.gui.test_vibration.tooltip"))
|
||||
.controller(ActionController::new)
|
||||
.action((screen, btn) -> {
|
||||
controller.rumbleManager().play(
|
||||
RumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
|
||||
.join(RumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
|
||||
.repeat(3)
|
||||
.join(RumbleEffect.constant(1f, 0f, 5).join(RumbleEffect.constant(0f, 1f, 5)).repeat(10))
|
||||
);
|
||||
})
|
||||
.build());;
|
||||
|
||||
category.group(advancedGroup.build());
|
||||
|
||||
var controlsGroup = OptionGroup.createBuilder()
|
||||
|
@ -6,17 +6,23 @@ import com.google.gson.JsonElement;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.rumble.RumbleCapable;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
import org.libsdl.SDL;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class AbstractController<S extends ControllerState, C extends ControllerConfig> implements Controller<S, C> {
|
||||
public abstract class AbstractController<S extends ControllerState, C extends ControllerConfig> implements Controller<S, C>, RumbleCapable {
|
||||
protected final int joystickId;
|
||||
protected String name;
|
||||
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;
|
||||
@ -30,12 +36,16 @@ 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 = UUID.nameUUIDFromBytes(hidInfo.path().get().getBytes()).toString();
|
||||
this.uid = hidInfo.createControllerUID().orElseThrow();
|
||||
this.type = hidInfo.type();
|
||||
} else {
|
||||
this.uid = "unidentified-guid-" + UUID.nameUUIDFromBytes(this.guid.getBytes());
|
||||
this.type = ControllerType.UNKNOWN;
|
||||
}
|
||||
this.type = hidInfo.type();
|
||||
|
||||
var joystickName = GLFW.glfwGetJoystickName(joystickId);
|
||||
String name = type != ControllerType.UNKNOWN || joystickName == null ? type.friendlyName() : joystickName;
|
||||
@ -53,8 +63,9 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
||||
protected void setName(String name) {
|
||||
String uniqueName = name;
|
||||
int i = 0;
|
||||
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equalsIgnoreCase)) {
|
||||
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(uniqueName::equalsIgnoreCase)) {
|
||||
uniqueName = name + " (" + i++ + ")";
|
||||
if (i > 1000) throw new IllegalStateException("Could not find a unique name for controller " + name + " (" + uid() + ")! (tried " + i + " times)");
|
||||
}
|
||||
this.name = uniqueName;
|
||||
}
|
||||
@ -105,6 +116,34 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setRumble(float strongMagnitude, float weakMagnitude) {
|
||||
if (!canRumble()) return false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@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;
|
||||
|
@ -7,10 +7,12 @@ import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
|
||||
String uid();
|
||||
@ -33,15 +35,22 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
void updateState();
|
||||
void clearState();
|
||||
|
||||
RumbleManager rumbleManager();
|
||||
|
||||
default boolean canBeUsed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void close() {
|
||||
|
||||
}
|
||||
|
||||
Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
|
||||
|
||||
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
if (CONTROLLERS.containsKey(hidInfo.createControllerUID())) {
|
||||
return CONTROLLERS.get(hidInfo.createControllerUID());
|
||||
Optional<String> uid = hidInfo.createControllerUID();
|
||||
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
||||
return CONTROLLERS.get(uid.get());
|
||||
}
|
||||
|
||||
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) {
|
||||
@ -55,6 +64,11 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
return controller;
|
||||
}
|
||||
|
||||
static void remove(Controller<?, ?> controller) {
|
||||
CONTROLLERS.remove(controller.uid());
|
||||
controller.close();
|
||||
}
|
||||
|
||||
Controller<?, ?> DUMMY = new Controller<>() {
|
||||
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
|
||||
private final ControllerConfig config = new ControllerConfig() {
|
||||
@ -133,5 +147,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
public void clearState() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RumbleManager rumbleManager() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ public abstract class ControllerConfig {
|
||||
|
||||
public float chatKeyboardHeight = 0f;
|
||||
|
||||
public boolean reduceBowSensitivity = true;
|
||||
public boolean reduceAimingSensitivity = true;
|
||||
|
||||
public boolean allowVibrations = true;
|
||||
|
||||
public boolean calibrated = false;
|
||||
|
||||
|
@ -78,8 +78,8 @@ public class ControllerHIDService implements HidServicesListener {
|
||||
}
|
||||
|
||||
public record ControllerHIDInfo(ControllerType type, Optional<String> path) {
|
||||
public String createControllerUID() {
|
||||
return UUID.nameUUIDFromBytes(path().get().getBytes()).toString();
|
||||
public Optional<String> createControllerUID() {
|
||||
return path.map(p -> UUID.nameUUIDFromBytes(p.getBytes())).map(UUID::toString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.ControllerType;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
|
||||
import dev.isxander.controlify.rumble.RumbleCapable;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CompoundJoystickController implements JoystickController<JoystickConfig> {
|
||||
public class CompoundJoystickController implements JoystickController<JoystickConfig>, RumbleCapable {
|
||||
private final String uid;
|
||||
private final List<Integer> joysticks;
|
||||
private final int axisCount, buttonCount, hatCount;
|
||||
@ -23,6 +25,8 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
|
||||
private JoystickConfig config;
|
||||
private final JoystickConfig defaultConfig;
|
||||
|
||||
private final RumbleManager rumbleManager;
|
||||
|
||||
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
|
||||
|
||||
public CompoundJoystickController(List<Integer> joystickIds, String uid, ControllerType compoundType) {
|
||||
@ -39,6 +43,8 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
|
||||
this.config = new JoystickConfig(this);
|
||||
this.defaultConfig = new JoystickConfig(this);
|
||||
|
||||
this.rumbleManager = new RumbleManager(this);
|
||||
|
||||
this.bindings = new ControllerBindings<>(this);
|
||||
}
|
||||
|
||||
@ -133,6 +139,21 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
|
||||
return this.hatCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setRumble(float strongMagnitude, float weakMagnitude) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRumble() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RumbleManager rumbleManager() {
|
||||
return this.rumbleManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeUsed() {
|
||||
return JoystickController.super.canBeUsed()
|
||||
|
@ -0,0 +1,137 @@
|
||||
package dev.isxander.controlify.controller.sdl2;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.Util;
|
||||
import org.libsdl.SDL;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SDL2NativesManager {
|
||||
private static final Path NATIVES_FOLDER = FabricLoader.getInstance().getGameDir().resolve("controlify-natives");
|
||||
private static final Map<Target, String> NATIVE_LIBRARIES = Map.of(
|
||||
new Target(Util.OS.WINDOWS, true), "windows64/sdl2gdx64.dll",
|
||||
new Target(Util.OS.WINDOWS, false), "windows32/sdl2gdx.dll",
|
||||
new Target(Util.OS.LINUX, true), "linux64/libsdl2gdx64.so",
|
||||
new Target(Util.OS.OSX, true), "macosx64/libsdl2gdx64.dylib"
|
||||
);
|
||||
private static final String NATIVE_LIBRARY_URL = "https://raw.githubusercontent.com/isXander/sdl2-jni/master/libs/";
|
||||
|
||||
private static Path osNativePath;
|
||||
private static boolean loaded = false;
|
||||
|
||||
public static void initialise() {
|
||||
if (loaded) return;
|
||||
|
||||
Controlify.LOGGER.info("Initialising SDL2 native library");
|
||||
|
||||
osNativePath = getNativesPathForOS().orElseGet(() -> {
|
||||
Controlify.LOGGER.warn("No native library found for SDL2");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (osNativePath == null) return;
|
||||
|
||||
if (!loadCachedLibrary()) {
|
||||
downloadLibrary();
|
||||
|
||||
if (!loadCachedLibrary()) {
|
||||
Controlify.LOGGER.warn("Failed to download and load SDL2 native library");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void startSDL2() {
|
||||
SDL.SDL_SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1");
|
||||
SDL.SDL_SetHint("SDL_ACCELEROMETER_AS_JOYSTICK", "0");
|
||||
SDL.SDL_SetHint("SDL_MAC_BACKGROUND_APP", "1");
|
||||
SDL.SDL_SetHint("SDL_XINPUT_ENABLED", "1");
|
||||
SDL.SDL_SetHint("SDL_JOYSTICK_RAWINPUT", "0");
|
||||
|
||||
int joystickSubsystem = 0x00000200; // implies event subsystem
|
||||
if (SDL.SDL_Init(joystickSubsystem) != 0) {
|
||||
Controlify.LOGGER.error("Failed to initialise SDL2: " + SDL.SDL_GetError());
|
||||
throw new RuntimeException("Failed to initialise SDL2: " + SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
Controlify.LOGGER.info("Initialised SDL2");
|
||||
}
|
||||
|
||||
private static boolean loadCachedLibrary() {
|
||||
if (!Files.exists(osNativePath)) return false;
|
||||
|
||||
Controlify.LOGGER.info("Loading SDL2 native library from " + osNativePath);
|
||||
|
||||
try {
|
||||
SDL.load(osNativePath);
|
||||
|
||||
startSDL2();
|
||||
|
||||
loaded = true;
|
||||
return true;
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean downloadLibrary() {
|
||||
Controlify.LOGGER.info("Downloading SDL2 native library");
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(osNativePath);
|
||||
Files.createDirectories(osNativePath.getParent());
|
||||
Files.createFile(osNativePath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
try(FileOutputStream fileOutputStream = new FileOutputStream(osNativePath.toFile())) {
|
||||
String downloadUrl = NATIVE_LIBRARY_URL + NATIVE_LIBRARIES.get(getNativeLibraryType());
|
||||
URL url = new URL(downloadUrl);
|
||||
ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
|
||||
FileChannel fileChannel = fileOutputStream.getChannel();
|
||||
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
|
||||
Controlify.LOGGER.info("Downloaded SDL2 native library from " + downloadUrl);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Target getNativeLibraryType() {
|
||||
Util.OS os = Util.getPlatform();
|
||||
boolean is64bit = System.getProperty("os.arch").contains("64");
|
||||
|
||||
return new Target(os, is64bit);
|
||||
}
|
||||
|
||||
private static Optional<Path> getNativesPathForOS() {
|
||||
String path = NATIVE_LIBRARIES.get(getNativeLibraryType());
|
||||
|
||||
if (path == null) {
|
||||
Controlify.LOGGER.warn("No native library found for SDL " + getNativeLibraryType());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(NATIVES_FOLDER.resolve(path));
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private record Target(Util.OS os, boolean is64Bit) {
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.ConfirmScreen;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public class VibrationOnboardingScreen extends ConfirmScreen {
|
||||
public VibrationOnboardingScreen(Screen lastScreen, BooleanConsumer onAnswered) {
|
||||
super(
|
||||
yes -> {
|
||||
Controlify.instance().config().globalSettings().loadVibrationNatives = yes;
|
||||
Controlify.instance().config().globalSettings().vibrationOnboarded = true;
|
||||
Controlify.instance().config().save();
|
||||
Minecraft.getInstance().setScreen(lastScreen);
|
||||
onAnswered.accept(yes);
|
||||
},
|
||||
Component.translatable("controlify.vibration_onboarding.title").withStyle(ChatFormatting.BOLD),
|
||||
Component.translatable("controlify.vibration_onboarding.message")
|
||||
);
|
||||
}
|
||||
}
|
@ -62,16 +62,23 @@ public class InGameInputHandler {
|
||||
}
|
||||
|
||||
protected void handlePlayerLookInput() {
|
||||
var player = this.minecraft.player;
|
||||
|
||||
var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state();
|
||||
var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
|
||||
|
||||
if (minecraft.mouseHandler.isMouseGrabbed() && minecraft.isWindowActive() && minecraft.player != null) {
|
||||
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().reduceBowSensitivity && minecraft.player.getUseItem().getItem() instanceof ProjectileWeaponItem) {
|
||||
lookInputX *= Math.abs(impulseX) * 0.6;
|
||||
lookInputY *= Math.abs(impulseY) * 0.6;
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
lookInputX = lookInputY = 0;
|
||||
|
@ -7,7 +7,7 @@ import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mixin(KeyBindingRegistryImpl.class)
|
||||
@Mixin(value = KeyBindingRegistryImpl.class, remap = false)
|
||||
public interface KeyBindingRegistryImplAccessor {
|
||||
@Accessor("MODDED_KEY_BINDINGS")
|
||||
static List<KeyMapping> getCustomKeys() {
|
||||
|
@ -28,13 +28,13 @@ public abstract class MinecraftMixin {
|
||||
@ModifyExpressionValue(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/ReloadableResourceManager;createReload(Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Ljava/util/List;)Lnet/minecraft/server/packs/resources/ReloadInstance;"))
|
||||
private ReloadInstance onInputInitialized(ReloadInstance resourceReload) {
|
||||
// Controllers need to be initialized extremely late due to the data-driven nature of controllers.
|
||||
resourceReload.done().thenRun(() -> Controlify.instance().initializeControllers());
|
||||
resourceReload.done().thenRun(() -> Controlify.instance().initializeControlify());
|
||||
return resourceReload;
|
||||
}
|
||||
|
||||
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyboardHandler;setup(J)V", shift = At.Shift.AFTER))
|
||||
private void onInputInitialized(CallbackInfo ci) {
|
||||
Controlify.instance().initializeControlify();
|
||||
Controlify.instance().preInitialiseControlify();
|
||||
}
|
||||
|
||||
@Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;turnPlayer()V"))
|
||||
|
@ -0,0 +1,31 @@
|
||||
package dev.isxander.controlify.mixins.feature.rumble.damage;
|
||||
|
||||
import dev.isxander.controlify.api.ControlifyApi;
|
||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LocalPlayer.class)
|
||||
public class LocalPlayerMixin {
|
||||
@Inject(method = "hurtTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setHealth(F)V", ordinal = 1))
|
||||
private void onClientHurt(float health, CallbackInfo ci) {
|
||||
// LivingEntity#hurt is server-side only, so we do it here
|
||||
doRumble();
|
||||
}
|
||||
|
||||
@Inject(method = "hurtTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setHealth(F)V", ordinal = 0))
|
||||
private void onClientHealthUpdate(float health, CallbackInfo ci) {
|
||||
// for some reason fall damage calls hurtTo after the health has been updated at some point
|
||||
// this is called when hurtTo is set to the same health as the player already has
|
||||
doRumble();
|
||||
}
|
||||
|
||||
private void doRumble() {
|
||||
ControlifyApi.get().currentController().rumbleManager().play(
|
||||
RumbleEffect.constant(0.5f, 0f, 5)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package dev.isxander.controlify.mixins.feature.rumble.sounds;
|
||||
|
||||
import dev.isxander.controlify.api.ControlifyApi;
|
||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||
import dev.isxander.controlify.rumble.RumbleState;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.LevelEvent;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LevelRenderer.class)
|
||||
public class LevelRendererMixin {
|
||||
@Inject(method = "levelEvent", at = @At("HEAD"))
|
||||
private void onLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
||||
switch (eventId) {
|
||||
case LevelEvent.SOUND_ANVIL_USED -> rumble(
|
||||
RumbleEffect.join(
|
||||
RumbleEffect.constant(1f, 0.5f, 2),
|
||||
RumbleEffect.empty(5)
|
||||
).repeat(3)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "globalLevelEvent", at = @At("HEAD"))
|
||||
private void onGlobalLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
||||
switch (eventId) {
|
||||
case LevelEvent.SOUND_DRAGON_DEATH -> rumble(
|
||||
RumbleEffect.join(
|
||||
RumbleEffect.constant(1f, 1f, 194),
|
||||
RumbleEffect.byTime(t -> {
|
||||
float easeOutQuad = 1 - (1 - t) * (1 - t);
|
||||
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
|
||||
}, 63)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void rumble(RumbleEffect effect) {
|
||||
ControlifyApi.get().currentController().rumbleManager().play(effect);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
||||
|
||||
import dev.isxander.controlify.api.ControlifyApi;
|
||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class LivingEntityMixin {
|
||||
@Shadow public abstract int getUseItemRemainingTicks();
|
||||
|
||||
@Inject(method = "updateUsingItem", at = @At("HEAD"))
|
||||
protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
||||
|
||||
import dev.isxander.controlify.api.ControlifyApi;
|
||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LocalPlayer.class)
|
||||
public abstract class LocalPlayerMixin extends LivingEntityMixin {
|
||||
@Override
|
||||
protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) {
|
||||
switch (stack.getUseAnimation()) {
|
||||
case BOW, CROSSBOW, SPEAR -> {
|
||||
var magnitude = Mth.clamp((stack.getUseDuration() - getUseItemRemainingTicks()) / 20f, 0f, 1f) * 0.5f;
|
||||
playRumble(RumbleEffect.constant(magnitude * 0.3f, magnitude, 1));
|
||||
}
|
||||
case BLOCK, SPYGLASS -> playRumble(RumbleEffect.constant(0f, 0.1f, 1));
|
||||
case EAT, DRINK -> playRumble(RumbleEffect.constant(0.05f, 0.1f, 1));
|
||||
case TOOT_HORN -> playRumble(RumbleEffect.constant(1f, 0.25f, 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void playRumble(RumbleEffect effect) {
|
||||
ControlifyApi.get().currentController().rumbleManager().play(effect);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package dev.isxander.controlify.rumble;
|
||||
|
||||
public interface RumbleCapable {
|
||||
boolean setRumble(float strongMagnitude, float weakMagnitude);
|
||||
|
||||
boolean canRumble();
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package dev.isxander.controlify.rumble;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public record RumbleEffect(RumbleState[] states) {
|
||||
/**
|
||||
* Creates a rumble effect where the state is determined by the tick.
|
||||
* @param stateFunction the function that takes a tick and returns the state for that tick.
|
||||
* @param durationTicks how many ticks the effect should last for.
|
||||
*/
|
||||
public static RumbleEffect byTick(Function<Integer, RumbleState> stateFunction, int durationTicks) {
|
||||
RumbleState[] states = new RumbleState[durationTicks];
|
||||
for (int i = 0; i < durationTicks; i++) {
|
||||
states[i] = stateFunction.apply(i);
|
||||
}
|
||||
return new RumbleEffect(states);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rumble effect from a function that takes a time value from 0, start, to 1, end, and returns that tick.
|
||||
* @param stateFunction the function that takes the time value and returns the state for that tick.
|
||||
* @param durationTicks how many ticks the effect should last for.
|
||||
*/
|
||||
public static RumbleEffect byTime(Function<Float, RumbleState> stateFunction, int durationTicks) {
|
||||
return RumbleEffect.byTick(tick -> stateFunction.apply((float) tick / (float) durationTicks), durationTicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rumble effect that has a constant state.
|
||||
* @param strong the strong motor magnitude.
|
||||
* @param weak the weak motor magnitude
|
||||
* @param durationTicks how many ticks the effect should last for.
|
||||
*/
|
||||
public static RumbleEffect constant(float strong, float weak, int durationTicks) {
|
||||
return RumbleEffect.byTick(tick -> new RumbleState(strong, weak), durationTicks);
|
||||
}
|
||||
|
||||
public static RumbleEffect empty(int durationTicks) {
|
||||
return RumbleEffect.byTick(tick -> new RumbleState(0f, 0f), durationTicks);
|
||||
}
|
||||
|
||||
public static RumbleEffect join(RumbleEffect... effects) {
|
||||
int totalTicks = 0;
|
||||
for (RumbleEffect effect : effects) {
|
||||
totalTicks += effect.states().length;
|
||||
}
|
||||
|
||||
RumbleState[] states = new RumbleState[totalTicks];
|
||||
int currentTick = 0;
|
||||
for (RumbleEffect effect : effects) {
|
||||
for (RumbleState state : effect.states()) {
|
||||
states[currentTick] = state;
|
||||
currentTick++;
|
||||
}
|
||||
}
|
||||
|
||||
return new RumbleEffect(states);
|
||||
}
|
||||
|
||||
public RumbleEffect join(RumbleEffect other) {
|
||||
return RumbleEffect.join(this, other);
|
||||
}
|
||||
|
||||
public RumbleEffect repeat(int count) {
|
||||
Validate.isTrue(count > 0, "count must be greater than 0");
|
||||
|
||||
if (count == 1) return this;
|
||||
|
||||
RumbleEffect effect = this;
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
effect = RumbleEffect.join(effect, this);
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package dev.isxander.controlify.rumble;
|
||||
|
||||
public class RumbleManager {
|
||||
private final RumbleCapable controller;
|
||||
private RumbleEffect playingEffect;
|
||||
private int currentPlayingTick;
|
||||
|
||||
public RumbleManager(RumbleCapable controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public void play(RumbleEffect effect) {
|
||||
if (!controller.canRumble())
|
||||
return;
|
||||
|
||||
playingEffect = effect;
|
||||
currentPlayingTick = 0;
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return playingEffect != null;
|
||||
}
|
||||
|
||||
public void stopCurrentEffect() {
|
||||
if (playingEffect == null)
|
||||
return;
|
||||
|
||||
controller.setRumble(0f, 0f);
|
||||
playingEffect = null;
|
||||
currentPlayingTick = 0;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (playingEffect == null)
|
||||
return;
|
||||
|
||||
if (currentPlayingTick >= playingEffect.states().length) {
|
||||
stopCurrentEffect();
|
||||
return;
|
||||
}
|
||||
|
||||
RumbleState state = playingEffect.states()[currentPlayingTick];
|
||||
controller.setRumble(state.strong(), state.weak());
|
||||
currentPlayingTick++;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package dev.isxander.controlify.rumble;
|
||||
|
||||
public record RumbleState(float strong, float weak) {
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
0x202,
|
||||
0x285,
|
||||
0x288,
|
||||
0xb13,
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -2,6 +2,9 @@
|
||||
"controlify.gui.category.global": "Global",
|
||||
"controlify.gui.current_controller": "Current Controller",
|
||||
"controlify.gui.current_controller.tooltip": "In Controlify's infancy, only one controller can be used at a time, this selects which one you want to use.",
|
||||
"controlify.gui.load_vibration_natives": "Vibration Support",
|
||||
"controlify.gui.load_vibration_natives.tooltip": "If enabled, Controlify will download and load native libraries on launch to enable vibration support. The download process only happens once and only downloads for your specific OS. Disabling this will not delete the natives, it just won't load them.",
|
||||
"controlify.gui.load_vibration_natives.tooltip.warning": "You must enable vibration support per-controller as well as this setting.",
|
||||
"controlify.gui.reach_around": "Block Reach Around",
|
||||
"controlify.gui.reach_around.tooltip": "If enabled, you can interact with the block you are standing on in the direction you are looking.",
|
||||
"controlify.gui.reach_around.tooltip.parity": "This is parity with bedrock edition where you can also do this.",
|
||||
@ -28,6 +31,8 @@
|
||||
"controlify.gui.toggle_sprint.tooltip": "How the state of the sprint button behaves.",
|
||||
"controlify.gui.auto_jump": "Auto Jump",
|
||||
"controlify.gui.auto_jump.tooltip": "If the player should automatically jump when you reach a block.",
|
||||
"controlify.gui.allow_vibrations": "Allow Vibration",
|
||||
"controlify.gui.allow_vibrations.tooltip": "If the controller should vibrate when you do certain actions.",
|
||||
"controlify.gui.show_ingame_guide": "Show Ingame Button Guide",
|
||||
"controlify.gui.show_ingame_guide.tooltip": "Show a HUD in-game displaying actions you can do with controller buttons.",
|
||||
"controlify.gui.show_screen_guide": "Show Screen Button Guide",
|
||||
@ -38,8 +43,8 @@
|
||||
"controlify.gui.chat_screen_offset.tooltip": "How far up the system on-screen keyboard is. This shifts up the chat box so you can see the chat whilst typing.\nThis is extremely useful on the steamdeck.",
|
||||
"controlify.gui.controller_theme": "Controller Theme",
|
||||
"controlify.gui.controller_theme.tooltip": "The theme to use for rendering controller buttons.",
|
||||
"controlify.gui.reduce_bow_sensitivity": "Reduce Bow Sensitivity",
|
||||
"controlify.gui.reduce_bow_sensitivity.tooltip": "Reduce the sensitivity of bow-like items when aiming.",
|
||||
"controlify.gui.reduce_aiming_sensitivity": "Reduce Aiming Sensitivity",
|
||||
"controlify.gui.reduce_aiming_sensitivity.tooltip": "Reduce the sensitivity when aiming.",
|
||||
"controlify.gui.custom_name": "Display Name",
|
||||
"controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.",
|
||||
"controlify.gui.group.advanced": "Advanced",
|
||||
@ -55,6 +60,8 @@
|
||||
"controlify.gui.auto_calibration.tooltip": "Automatically calibrate the deadzone of your controller.",
|
||||
"controlify.gui.button_activation_threshold": "Button Activation Threshold",
|
||||
"controlify.gui.button_activation_threshold.tooltip": "How far a button needs to be pushed before registering as pressed.",
|
||||
"controlify.gui.test_vibration": "Test Vibration",
|
||||
"controlify.gui.test_vibration.tooltip": "Test the vibration of your controller.",
|
||||
|
||||
"controlify.gui.group.controls": "Controls",
|
||||
"controlify.gui.group.controls.tooltip": "Adjust the controller controls.",
|
||||
@ -80,6 +87,9 @@
|
||||
"controlify.toast.faulty_input.title": "Controller disabled",
|
||||
"controlify.toast.faulty_input.description": "Your controller has been disabled because Controlify detected it was causing you problems using keyboard and mouse input. This is likely due to setting your deadzone values too low or your joystick is not mapped properly, making the controller think it is always giving input.",
|
||||
|
||||
"controlify.vibration_onboarding.title": "Controlify Vibration Support",
|
||||
"controlify.vibration_onboarding.message": "To enable vibration support, a native library must be downloaded that Controlify loads automatically. This is a seamless process and will only take a few seconds. If you choose no, you may change your mind later in Controlify settings.\n\nWould you like to download them?",
|
||||
|
||||
"controlify.controller_theme.default": "Default",
|
||||
"controlify.controller_theme.xbox_one": "Xbox",
|
||||
"controlify.controller_theme.dualshock4": "PS4",
|
||||
|
@ -8,7 +8,9 @@
|
||||
"compat.sodium.CycleControlElementMixin",
|
||||
"compat.sodium.SliderControlElementMixin",
|
||||
"compat.sodium.TickBoxControlElementMixin",
|
||||
"core.GLXMixin"
|
||||
"core.GLXMixin",
|
||||
"feature.rumble.sounds.LevelRendererMixin",
|
||||
"feature.rumble.useitem.LivingEntityMixin"
|
||||
],
|
||||
"client": [
|
||||
"compat.fapi.KeyBindingRegistryImplAccessor",
|
||||
@ -33,6 +35,8 @@
|
||||
"feature.guide.screen.AbstractWidgetMixin",
|
||||
"feature.guide.screen.TabNavigationBarMixin",
|
||||
"feature.reacharound.GameRendererMixin",
|
||||
"feature.rumble.damage.LocalPlayerMixin",
|
||||
"feature.rumble.useitem.LocalPlayerMixin",
|
||||
"feature.screenop.MinecraftMixin",
|
||||
"feature.screenop.ScreenMixin",
|
||||
"feature.screenop.vanilla.AbstractButtonMixin",
|
||||
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@ -44,10 +45,18 @@ public class ClientTestHelper {
|
||||
return Minecraft.getInstance().submit(() -> function.apply(Minecraft.getInstance()));
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> submit(Consumer<Minecraft> consumer) {
|
||||
return Minecraft.getInstance().submit(() -> consumer.accept(Minecraft.getInstance()));
|
||||
}
|
||||
|
||||
public static <T> T submitAndWait(Function<Minecraft, T> function) {
|
||||
return submit(function).join();
|
||||
}
|
||||
|
||||
public static void submitConsumerAndWait(Consumer<Minecraft> consumer) {
|
||||
submit(consumer).join();
|
||||
}
|
||||
|
||||
public static void takeScreenshot(String name) {
|
||||
AtomicBoolean returned = new AtomicBoolean(false);
|
||||
submitAndWait(mc -> {
|
||||
|
@ -2,6 +2,8 @@ package dev.isxander.controlify.test;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -71,6 +73,7 @@ public class ControlifyAutoTestClient implements ClientModInitializer {
|
||||
}
|
||||
|
||||
waitForLoadingComplete();
|
||||
submitConsumerAndWait(client -> client.setScreen(new TitleScreen()));
|
||||
|
||||
for (var test : postLoadTests) {
|
||||
success &= wrapTestExecution(test);
|
||||
|
@ -11,6 +11,8 @@ import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.JoystickState;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||
import dev.isxander.controlify.rumble.RumbleCapable;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -22,6 +24,7 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
private final ControllerBindings<JoystickState> bindings;
|
||||
private final JoystickConfig config;
|
||||
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
|
||||
private final RumbleManager rumbleManager;
|
||||
|
||||
private float axisState;
|
||||
private boolean shouldClearAxisNextTick;
|
||||
@ -34,6 +37,17 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
this.id = -JOYSTICK_COUNT;
|
||||
this.bindings = new ControllerBindings<>(this);
|
||||
this.config = new JoystickConfig(this);
|
||||
this.rumbleManager = new RumbleManager(new RumbleCapable() {
|
||||
@Override
|
||||
public boolean setRumble(float strongMagnitude, float weakMagnitude) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRumble() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
this.config.calibrated = true;
|
||||
}
|
||||
|
||||
@ -114,6 +128,11 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
state = JoystickState.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RumbleManager rumbleManager() {
|
||||
return rumbleManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JoystickMapping mapping() {
|
||||
return UnmappedJoystickMapping.INSTANCE;
|
||||
@ -170,6 +189,11 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
JoystickController.super.close();
|
||||
}
|
||||
|
||||
public static class FakeControllerState extends JoystickState {
|
||||
protected FakeControllerState(JoystickMapping mapping, float axis, boolean button, HatState hat) {
|
||||
super(mapping, List.of(axis), List.of(axis), List.of(button), List.of(hat));
|
||||
|
Reference in New Issue
Block a user