forked from Clones/Controlify
🎮📳 Controller Vibration! (#38)
This commit is contained in:
@ -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) {
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user